Compare commits
No commits in common. "1b98f6a7162dc3835ffa6cb3d16924606eb5fece" and "b925518e69898d168c6874760881c9fb7870f147" have entirely different histories.
1b98f6a716
...
b925518e69
@ -37,7 +37,6 @@
|
|||||||
.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,
|
||||||
@ -506,7 +505,7 @@ body {
|
|||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
flex: 0 0 100% !important;
|
flex: 0 0 100% !important;
|
||||||
transition:
|
transition:
|
||||||
all 0.3s cubic-bezier(0.2, 0, 0, 1) 0s,
|
all 0.3s,
|
||||||
background 0s;
|
background 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,13 +528,11 @@ 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;
|
||||||
}
|
}
|
||||||
@ -941,18 +938,41 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
.sidebar-scroll .simplebar-track.simplebar-vertical {
|
||||||
top: 8px;
|
right: -1px;
|
||||||
right: calc(-1 * var(--scrollbox-vertical-right-padding) - 0.5px);
|
width: 0px !important;
|
||||||
width: 8px !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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-scroll
|
.sidebar-scroll
|
||||||
.simplebar-track.simplebar-vertical
|
.simplebar-track.simplebar-vertical
|
||||||
.simplebar-scrollbar:before {
|
.simplebar-scrollbar:before {
|
||||||
top: 2px;
|
top: 4px;
|
||||||
bottom: 2px;
|
bottom: 4px;
|
||||||
left: 2px;
|
left: 4px;
|
||||||
right: 2px;
|
right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-color-picker
|
.ant-color-picker
|
||||||
@ -1076,229 +1096,29 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adaptive-splitter {
|
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning
|
||||||
display: flex;
|
.ant-splitter-panel {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
align-items: stretch;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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:
|
transition:
|
||||||
flex-basis var(--adaptive-splitter-size-transition, 0.3s)
|
--dashboard-splitter-sidebar-size 0.3s ease-in-out,
|
||||||
cubic-bezier(0.2, 0, 0, 1),
|
flex-basis 0.3s ease-in-out,
|
||||||
width var(--adaptive-splitter-size-transition, 0.3s)
|
width 0.3s ease-in-out,
|
||||||
cubic-bezier(0.2, 0, 0, 1),
|
min-width 0.3s ease-in-out,
|
||||||
min-width var(--adaptive-splitter-size-transition, 0.3s)
|
max-width 0.3s ease-in-out;
|
||||||
cubic-bezier(0.2, 0, 0, 1),
|
}
|
||||||
max-width var(--adaptive-splitter-size-transition, 0.3s)
|
|
||||||
cubic-bezier(0.2, 0, 0, 1),
|
@property --dashboard-splitter-sidebar-size {
|
||||||
height var(--adaptive-splitter-size-transition, 0.3s)
|
syntax: '<length>';
|
||||||
cubic-bezier(0.2, 0, 0, 1),
|
inherits: true;
|
||||||
min-height var(--adaptive-splitter-size-transition, 0.3s)
|
initial-value: 80px;
|
||||||
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
|
||||||
@ -1823,34 +1643,41 @@ 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,
|
||||||
|
|||||||
@ -21,15 +21,14 @@ import { AppUpdateContext } from '../context/AppUpdateContext'
|
|||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import FarmControlAppIcon from '../../Logos/FarmControlAppIcon'
|
import FarmControlAppIcon from '../../Logos/FarmControlAppIcon'
|
||||||
import SoftwareUpdateIcon from '../../Icons/SoftwareUpdateIcon'
|
import SoftwareUpdateIcon from '../../Icons/SoftwareUpdateIcon'
|
||||||
import ExternalLink from '../common/ExternalLink'
|
const { Title, Text, Link } = Typography
|
||||||
const { Title, Text } = Typography
|
|
||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
const [collapseState, updateCollapseState] = useCollapseState('About', {
|
const [collapseState, updateCollapseState] = useCollapseState('About', {
|
||||||
updater: true
|
updater: true
|
||||||
})
|
})
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
const { fetchApiServerVersion, fetchWsServerVersion, connected } =
|
const { fetchApiServerVersion, fetchWsServerVersion } =
|
||||||
useContext(ApiServerContext)
|
useContext(ApiServerContext)
|
||||||
const { isElectron, getAppEngine } = useContext(ElectronContext)
|
const { isElectron, getAppEngine } = useContext(ElectronContext)
|
||||||
const { checkForUpdates } = useContext(AppUpdateContext)
|
const { checkForUpdates } = useContext(AppUpdateContext)
|
||||||
@ -62,7 +61,7 @@ const About = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token && connected) {
|
if (token) {
|
||||||
fetchApiServerVersion().then((version) => {
|
fetchApiServerVersion().then((version) => {
|
||||||
setApiServerVersion(version)
|
setApiServerVersion(version)
|
||||||
})
|
})
|
||||||
@ -70,7 +69,7 @@ const About = () => {
|
|||||||
setWsServerVersion(version)
|
setWsServerVersion(version)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [fetchApiServerVersion, fetchWsServerVersion, token, connected])
|
}, [fetchApiServerVersion, fetchWsServerVersion, token])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isElectron) return
|
if (!isElectron) return
|
||||||
@ -172,13 +171,11 @@ const About = () => {
|
|||||||
Development
|
Development
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
<ExternalLink href='https://ci.tombutcher.work/job/farmcontrol'>
|
<Link href='https://ci.tombutcher.work/job/farmcontrol'>
|
||||||
Jenkins
|
Jenkins
|
||||||
</ExternalLink>
|
</Link>
|
||||||
<Divider type='vertical' style={{ margin: 0 }} />
|
<Divider type='vertical' style={{ margin: 0 }} />
|
||||||
<ExternalLink href='https://git.tombutcher.work/farmcontrol'>
|
<Link href='https://github.com/farmcontrol'>GitHub</Link>
|
||||||
Git
|
|
||||||
</ExternalLink>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -1,981 +0,0 @@
|
|||||||
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,21 +3,22 @@ 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 } from 'antd'
|
import { Layout, Splitter } 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 = 200
|
const COLLAPSE_BELOW_PX = 150
|
||||||
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 = 150
|
||||||
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
|
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
|
||||||
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
|
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
|
||||||
|
|
||||||
@ -55,7 +56,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 collapsedWidth
|
return isElectron ? ELECTRON_COLLAPSED_WIDTH_PX : COLLAPSED_WIDTH_PX
|
||||||
}
|
}
|
||||||
return clampExpandedSize(
|
return clampExpandedSize(
|
||||||
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
|
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
|
||||||
@ -67,6 +68,118 @@ 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))
|
||||||
@ -79,73 +192,118 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
|||||||
const collapseSidebar = useCallback(
|
const collapseSidebar = useCallback(
|
||||||
(expandedSize) => {
|
(expandedSize) => {
|
||||||
if (expandedSize >= COLLAPSE_BELOW_PX) {
|
if (expandedSize >= COLLAPSE_BELOW_PX) {
|
||||||
lastExpandedSizeRef.current = clampExpandedSize(expandedSize)
|
lastExpandedSizeRef.current = expandedSize
|
||||||
persistSize(lastExpandedSizeRef.current)
|
persistSize(expandedSize)
|
||||||
}
|
}
|
||||||
collapsedRef.current = true
|
collapsedRef.current = true
|
||||||
persistCollapsed(true)
|
persistCollapsed(true)
|
||||||
|
startSizeTransition()
|
||||||
|
setSidebarSize(sidebarSizeRef.current)
|
||||||
setCollapsed(true)
|
setCollapsed(true)
|
||||||
setSidebarSize(collapsedWidth)
|
animateCollapsedSize()
|
||||||
},
|
},
|
||||||
[collapsedWidth]
|
[animateCollapsedSize, startSizeTransition]
|
||||||
)
|
)
|
||||||
|
|
||||||
const expandSidebar = useCallback(() => {
|
const expandSidebar = useCallback(() => {
|
||||||
|
cancelCollapseSizeAnimation()
|
||||||
collapsedRef.current = false
|
collapsedRef.current = false
|
||||||
setCollapsed(false)
|
setCollapsed(false)
|
||||||
persistCollapsed(false)
|
persistCollapsed(false)
|
||||||
const nextSize = clampExpandedSize(lastExpandedSizeRef.current)
|
startSizeTransition()
|
||||||
lastExpandedSizeRef.current = nextSize
|
setSidebarSize(DEFAULT_SIDEBAR_PX)
|
||||||
setSidebarSize(nextSize)
|
persistSize(DEFAULT_SIDEBAR_PX)
|
||||||
persistSize(nextSize)
|
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX
|
||||||
}, [])
|
}, [cancelCollapseSizeAnimation, startSizeTransition])
|
||||||
|
|
||||||
const handleSidebarCollapse = useCallback(
|
const handleSidebarCollapse = useCallback(
|
||||||
(nextCollapsed) => {
|
(nextCollapsed) => {
|
||||||
if (nextCollapsed === collapsedRef.current) return
|
|
||||||
if (nextCollapsed) {
|
if (nextCollapsed) {
|
||||||
collapseSidebar(lastExpandedSizeRef.current)
|
collapseSidebar(sidebarSize)
|
||||||
} else {
|
} else {
|
||||||
expandSidebar()
|
expandSidebar()
|
||||||
}
|
}
|
||||||
|
lockResizeFor(SIZE_TRANSITION_MS)
|
||||||
},
|
},
|
||||||
[collapseSidebar, expandSidebar]
|
[collapseSidebar, expandSidebar, lockResizeFor, sidebarSize]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSplitterCollapse = useCallback(
|
const handleResizeStart = useCallback(() => {
|
||||||
(nextCollapsed) => {
|
if (!resizeLockedRef.current) {
|
||||||
if (nextCollapsed === collapsedRef.current) return
|
ignoreResizeEndRef.current = false
|
||||||
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 handleResizeEnd = useCallback(
|
const handleResize = useCallback(
|
||||||
(sizes) => {
|
(sizes) => {
|
||||||
if (collapsedRef.current) {
|
if (resizeLockedRef.current || suppressGestureRef.current) {
|
||||||
setSidebarSize(collapsedWidth)
|
if (collapsedRef.current) {
|
||||||
|
setSidebarSize(collapsedWidth)
|
||||||
|
}
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
|
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]
|
[collapsedWidth, expandSidebar, lockResizeFor, startSizeTransition]
|
||||||
)
|
)
|
||||||
|
|
||||||
const sidebarNode = isValidElement(sidebar)
|
const sidebarNode = isValidElement(sidebar)
|
||||||
@ -167,38 +325,38 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ height: '100%', minHeight: 0 }}>
|
<Layout style={{ height: '100%', minHeight: 0 }}>
|
||||||
<AdaptiveSplitter
|
<Splitter
|
||||||
className={`farmcontrol-splitter dashboard-splitter${
|
className={`farmcontrol-splitter dashboard-splitter${
|
||||||
collapsed ? ' is-sidebar-collapsed' : ''
|
sizeTransitioning ? ' is-size-transitioning' : ''
|
||||||
}`}
|
}${collapsed ? ' is-sidebar-collapsed' : ''}`}
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
|
onResizeStart={handleResizeStart}
|
||||||
onResize={handleResize}
|
onResize={handleResize}
|
||||||
onResizeEnd={handleResizeEnd}
|
onResizeEnd={handleResizeEnd}
|
||||||
>
|
>
|
||||||
<AdaptiveSplitter.Panel
|
<Splitter.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_SIDEBAR_PX}
|
max={
|
||||||
collapsed={collapsed}
|
collapsed && resizeLocked && !sizeTransitioning
|
||||||
collapsedSize={collapsedWidth}
|
? collapsedWidth
|
||||||
collapseThreshold={COLLAPSE_BELOW_PX}
|
: MAX_SIDEBAR_PX
|
||||||
expandDragPx={EXPAND_DRAG_PX}
|
}
|
||||||
expandedSize={lastExpandedSizeRef.current}
|
resizable={!resizeLocked}
|
||||||
onCollapse={handleSplitterCollapse}
|
style={{
|
||||||
style={{ overflow: 'visible' }}
|
overflow: 'visible',
|
||||||
|
'--dashboard-splitter-sidebar-size': `${sidebarSize}px`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{sidebarNode}
|
{sidebarNode}
|
||||||
</AdaptiveSplitter.Panel>
|
</Splitter.Panel>
|
||||||
<AdaptiveSplitter.Panel
|
<Splitter.Panel min={320} className='dashboard-splitter-content-panel'>
|
||||||
min={320}
|
|
||||||
className='dashboard-splitter-content-panel'
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</AdaptiveSplitter.Panel>
|
</Splitter.Panel>
|
||||||
</AdaptiveSplitter>
|
</Splitter>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
import { useContext } from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Typography } from 'antd'
|
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
|
||||||
|
|
||||||
const { Link } = Typography
|
|
||||||
|
|
||||||
const ExternalLink = ({ href, children, onClick, ...props }) => {
|
|
||||||
const { isElectron, openExternalUrl } = useContext(ElectronContext)
|
|
||||||
|
|
||||||
const handleClick = (event) => {
|
|
||||||
onClick?.(event)
|
|
||||||
if (event.defaultPrevented) return
|
|
||||||
|
|
||||||
if (isElectron) {
|
|
||||||
event.preventDefault()
|
|
||||||
openExternalUrl(href)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
{...props}
|
|
||||||
href={href}
|
|
||||||
target={isElectron ? undefined : '_blank'}
|
|
||||||
rel={isElectron ? undefined : 'noopener noreferrer'}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ExternalLink.propTypes = {
|
|
||||||
href: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onClick: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExternalLink
|
|
||||||
@ -4,9 +4,8 @@ import Tooltip from './Tooltip'
|
|||||||
import LinkIcon from '../../Icons/LinkIcon'
|
import LinkIcon from '../../Icons/LinkIcon'
|
||||||
import CopyButton from './CopyButton'
|
import CopyButton from './CopyButton'
|
||||||
import ElipsisText from './ElipsisText'
|
import ElipsisText from './ElipsisText'
|
||||||
import ExternalLink from './ExternalLink'
|
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text, Link } = Typography
|
||||||
|
|
||||||
const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
||||||
if (!url) return <Text type='secondary'>n/a</Text>
|
if (!url) return <Text type='secondary'>n/a</Text>
|
||||||
@ -15,8 +14,10 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
|||||||
<>
|
<>
|
||||||
<Flex style={{ minWidth: 0 }}>
|
<Flex style={{ minWidth: 0 }}>
|
||||||
{showLink ? (
|
{showLink ? (
|
||||||
<ExternalLink
|
<Link
|
||||||
href={url}
|
href={url}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
style={{
|
style={{
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
@ -28,7 +29,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
|||||||
<ElipsisText style={{ display: 'block' }}>
|
<ElipsisText style={{ display: 'block' }}>
|
||||||
{url}
|
{url}
|
||||||
</ElipsisText>
|
</ElipsisText>
|
||||||
</ExternalLink>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ElipsisText style={{ marginRight: 8, minWidth: 0 }}>
|
<ElipsisText style={{ marginRight: 8, minWidth: 0 }}>
|
||||||
|
|||||||
@ -603,43 +603,29 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
}, [token, authenticated, connectionIssue, scheduleReconnect])
|
}, [token, authenticated, connectionIssue, scheduleReconnect])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const reconnectIfDisconnected = (reason) => {
|
|
||||||
if (!token || authenticated !== true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const socket = socketRef.current
|
|
||||||
if (socket?.connected) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`${reason} with disconnected socket, reconnecting...`)
|
|
||||||
reconnectAttemptRef.current = 0
|
|
||||||
attemptReconnect()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageVisible = () => {
|
const handlePageVisible = () => {
|
||||||
if (document.visibilityState !== 'visible') {
|
if (document.visibilityState !== 'visible') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reconnectIfDisconnected('Page visible')) {
|
const socket = socketRef.current
|
||||||
|
if (!socket || !token || authenticated !== true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = socketRef.current
|
if (!socket.connected) {
|
||||||
if (socket && subscribedActivityCallbacksRef.current.size > 0) {
|
logger.debug('Page visible with disconnected socket, reconnecting...')
|
||||||
|
attemptReconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscribedActivityCallbacksRef.current.size > 0) {
|
||||||
logger.debug('Page visible, resubscribing activity listeners...')
|
logger.debug('Page visible, resubscribing activity listeners...')
|
||||||
subscribedActivityServerSubscriptionsRef.current.clear()
|
subscribedActivityServerSubscriptionsRef.current.clear()
|
||||||
resubscribeActivityListeners(socket)
|
resubscribeActivityListeners(socket)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWindowFocus = () => {
|
|
||||||
reconnectIfDisconnected('Window focused')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageShow = (event) => {
|
const handlePageShow = (event) => {
|
||||||
if (!event.persisted) {
|
if (!event.persisted) {
|
||||||
return
|
return
|
||||||
@ -653,12 +639,10 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', handlePageVisible)
|
document.addEventListener('visibilitychange', handlePageVisible)
|
||||||
window.addEventListener('focus', handleWindowFocus)
|
|
||||||
window.addEventListener('pageshow', handlePageShow)
|
window.addEventListener('pageshow', handlePageShow)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('visibilitychange', handlePageVisible)
|
document.removeEventListener('visibilitychange', handlePageVisible)
|
||||||
window.removeEventListener('focus', handleWindowFocus)
|
|
||||||
window.removeEventListener('pageshow', handlePageShow)
|
window.removeEventListener('pageshow', handlePageShow)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user