resizable widget container

This commit is contained in:
Jan Prochazka 2020-05-10 17:39:49 +02:00
parent dc87aeeb43
commit 836db096a9
6 changed files with 100 additions and 46 deletions

View File

@ -7,6 +7,7 @@ import {
OpenedTabsProvider, OpenedTabsProvider,
SavedSqlFilesProvider, SavedSqlFilesProvider,
OpenedConnectionsProvider, OpenedConnectionsProvider,
LeftPanelWidthProvider,
} from './utility/globalState'; } from './utility/globalState';
import { SocketProvider } from './utility/SocketProvider'; import { SocketProvider } from './utility/SocketProvider';
import ConnectionsPinger from './utility/ConnectionsPinger'; import ConnectionsPinger from './utility/ConnectionsPinger';
@ -19,9 +20,11 @@ function App() {
<OpenedTabsProvider> <OpenedTabsProvider>
<SavedSqlFilesProvider> <SavedSqlFilesProvider>
<OpenedConnectionsProvider> <OpenedConnectionsProvider>
<ConnectionsPinger> <LeftPanelWidthProvider>
<Screen /> <ConnectionsPinger>
</ConnectionsPinger> <Screen />
</ConnectionsPinger>
</LeftPanelWidthProvider>
</OpenedConnectionsProvider> </OpenedConnectionsProvider>
</SavedSqlFilesProvider> </SavedSqlFilesProvider>
</OpenedTabsProvider> </OpenedTabsProvider>

View File

@ -6,15 +6,16 @@ import styled from 'styled-components';
import TabsPanel from './TabsPanel'; import TabsPanel from './TabsPanel';
import TabContent from './TabContent'; import TabContent from './TabContent';
import WidgetIconPanel from './widgets/WidgetIconPanel'; import WidgetIconPanel from './widgets/WidgetIconPanel';
import { useCurrentWidget } from './utility/globalState'; import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState';
import WidgetContainer from './widgets/WidgetContainer'; import WidgetContainer from './widgets/WidgetContainer';
import ToolBar from './widgets/Toolbar'; import ToolBar from './widgets/Toolbar';
import StatusBar from './widgets/StatusBar'; import StatusBar from './widgets/StatusBar';
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
const BodyDiv = styled.div` const BodyDiv = styled.div`
position: fixed; position: fixed;
top: ${theme.tabsPanel.height + theme.toolBar.height}px; top: ${theme.tabsPanel.height + theme.toolBar.height}px;
left: ${props => theme.widgetMenu.iconSize + props.leftPanelWidth}px; left: ${(props) => props.contentLeft}px;
bottom: ${theme.statusBar.height}px; bottom: ${theme.statusBar.height}px;
right: 0; right: 0;
background-color: ${theme.mainArea.background}; background-color: ${theme.mainArea.background};
@ -43,7 +44,6 @@ const LeftPanel = styled.div`
top: ${theme.toolBar.height}px; top: ${theme.toolBar.height}px;
left: ${theme.widgetMenu.iconSize}px; left: ${theme.widgetMenu.iconSize}px;
bottom: ${theme.statusBar.height}px; bottom: ${theme.statusBar.height}px;
width: ${theme.leftPanel.width}px;
background-color: ${theme.leftPanel.background}; background-color: ${theme.leftPanel.background};
display: flex; display: flex;
`; `;
@ -52,11 +52,11 @@ const TabsPanelContainer = styled.div`
display: flex; display: flex;
position: fixed; position: fixed;
top: ${theme.toolBar.height}px; top: ${theme.toolBar.height}px;
left: ${props => theme.widgetMenu.iconSize + props.leftPanelWidth}px; left: ${(props) => props.contentLeft}px;
height: ${theme.tabsPanel.height}px; height: ${theme.tabsPanel.height}px;
right: 0; right: 0;
background-color: ${theme.tabsPanel.background}; background-color: ${theme.tabsPanel.background};
border-top: 1px solid #CCC; border-top: 1px solid #ccc;
`; `;
const StausBarContainer = styled.div` const StausBarContainer = styled.div`
@ -68,10 +68,21 @@ const StausBarContainer = styled.div`
background-color: ${theme.statusBar.background}; background-color: ${theme.statusBar.background};
`; `;
const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)`
position: absolute;
top: ${theme.toolBar.height}px;
bottom: ${theme.statusBar.height}px;
`;
export default function Screen() { export default function Screen() {
const currentWidget = useCurrentWidget(); const currentWidget = useCurrentWidget();
const leftPanelWidth = currentWidget ? theme.leftPanel.width : 0; const leftPanelWidth = useLeftPanelWidth();
const setLeftPanelWidth = useSetLeftPanelWidth();
const contentLeft = currentWidget
? theme.widgetMenu.iconSize + leftPanelWidth + theme.splitter.thickness
: theme.widgetMenu.iconSize;
const toolbarPortalRef = React.useRef(); const toolbarPortalRef = React.useRef();
const onSplitDown = useSplitterDrag('clientX', (diff) => setLeftPanelWidth((v) => v + diff));
return ( return (
<> <>
<ToolBarDiv> <ToolBarDiv>
@ -85,10 +96,16 @@ export default function Screen() {
<WidgetContainer /> <WidgetContainer />
</LeftPanel> </LeftPanel>
)} )}
<TabsPanelContainer leftPanelWidth={leftPanelWidth}> {!!currentWidget && (
<ScreenHorizontalSplitHandle
onMouseDown={onSplitDown}
style={{ left: leftPanelWidth + theme.widgetMenu.iconSize }}
/>
)}
<TabsPanelContainer contentLeft={contentLeft}>
<TabsPanel></TabsPanel> <TabsPanel></TabsPanel>
</TabsPanelContainer> </TabsPanelContainer>
<BodyDiv leftPanelWidth={leftPanelWidth}> <BodyDiv contentLeft={contentLeft}>
<TabContent toolbarPortalRef={toolbarPortalRef} /> <TabContent toolbarPortalRef={toolbarPortalRef} />
</BodyDiv> </BodyDiv>
<StausBarContainer> <StausBarContainer>

View File

@ -1,30 +1,33 @@
export default { export default {
widgetMenu: { widgetMenu: {
iconSize: 60, iconSize: 60,
background: "#222", background: '#222',
iconFontSize: "23pt", iconFontSize: '23pt',
iconFontColor: "#eee", iconFontColor: '#eee',
backgroundHover: "#555", backgroundHover: '#555',
backgroundSelected: "#4CAF50", backgroundSelected: '#4CAF50',
}, },
leftPanel: { leftPanel: {
width: 300, // width: 300,
background: "#ccc" background: '#ccc',
}, },
tabsPanel: { tabsPanel: {
height: 31, height: 31,
background: "#ddd", background: '#ddd',
hoverFont: "#338" hoverFont: '#338',
}, },
statusBar: { statusBar: {
height: 20, height: 20,
background: "#00c" background: '#00c',
}, },
toolBar: { toolBar: {
height: 30, height: 30,
background: "#eee", background: '#eee',
}, },
mainArea: { mainArea: {
background: "#eee" background: '#eee',
} },
splitter: {
thickness: 3,
},
}; };

View File

@ -104,3 +104,7 @@ export { SavedSqlFilesProvider, useSavedSqlFiles, useSetSavedSqlFiles };
const [OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections] = createGlobalState([]); const [OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections] = createGlobalState([]);
export { OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections }; export { OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections };
const [LeftPanelWidthProvider, useLeftPanelWidth, useSetLeftPanelWidth] = createGlobalState(300);
export { LeftPanelWidthProvider, useLeftPanelWidth, useSetLeftPanelWidth };

View File

@ -2,6 +2,7 @@ import _ from 'lodash';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import useDimensions from '../utility/useDimensions'; import useDimensions from '../utility/useDimensions';
import theme from '../theme';
const MainContainer = styled.div` const MainContainer = styled.div`
flex: 1; flex: 1;
@ -9,13 +10,20 @@ const MainContainer = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const VerticalSplitHandle = styled.div` export const VerticalSplitHandle = styled.div`
background-color: #ccc; background-color: #ccc;
height: 4px; height: ${theme.splitter.thickness}px;
cursor: row-resize; cursor: row-resize;
z-index: 1; z-index: 1;
`; `;
export const HorizontalSplitHandle = styled.div`
background-color: #ccc;
width: ${theme.splitter.thickness}px;
cursor: col-resize;
z-index: 1;
`;
const ChildContainer1 = styled.div` const ChildContainer1 = styled.div`
// flex: 0 0 50%; // flex: 0 0 50%;
// flex-basis: 100px; // flex-basis: 100px;
@ -33,27 +41,16 @@ const ChildContainer2 = styled.div`
position: relative; position: relative;
`; `;
export function VerticalSplitter({ children }) { export function useSplitterDrag(axes, onResize) {
const childrenArray = _.isArray(children) ? children : [children];
if (childrenArray.length !== 1 && childrenArray.length != 2) {
throw new Error('Splitter must have 1 or 2 children');
}
const [refNode, dimensions] = useDimensions();
const [height1, setHeight1] = React.useState(0);
React.useEffect(() => {
setHeight1(dimensions.height / 2);
}, [dimensions]);
const [resizeStart, setResizeStart] = React.useState(null); const [resizeStart, setResizeStart] = React.useState(null);
React.useEffect(() => { React.useEffect(() => {
if (resizeStart != null) { if (resizeStart != null) {
const handleResizeMove = (e) => { const handleResizeMove = (e) => {
e.preventDefault(); e.preventDefault();
let diff = e.clientY - resizeStart; let diff = e[axes] - resizeStart;
setResizeStart(e.clientY); setResizeStart(e[axes]);
setHeight1((v) => v + diff); onResize(diff);
}; };
const handleResizeEnd = (e) => { const handleResizeEnd = (e) => {
e.preventDefault(); e.preventDefault();
@ -71,9 +68,26 @@ export function VerticalSplitter({ children }) {
}, [resizeStart]); }, [resizeStart]);
const handleResizeDown = (e) => { const handleResizeDown = (e) => {
setResizeStart(e.clientY); setResizeStart(e[axes]);
}; };
return handleResizeDown;
}
export function VerticalSplitter({ children }) {
const childrenArray = _.isArray(children) ? children : [children];
if (childrenArray.length !== 1 && childrenArray.length != 2) {
throw new Error('Splitter must have 1 or 2 children');
}
const [refNode, dimensions] = useDimensions();
const [height1, setHeight1] = React.useState(0);
React.useEffect(() => {
setHeight1(dimensions.height / 2);
}, [dimensions]);
const handleResizeDown = useSplitterDrag('clientY', (diff) => setHeight1((v) => v + diff));
const isSplitter = !!childrenArray[1]; const isSplitter = !!childrenArray[1];
return ( return (

View File

@ -1,5 +1,8 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import theme from '../theme'; import theme from '../theme';
import { useLeftPanelWidth } from '../utility/globalState';
export const SearchBoxWrapper = styled.div` export const SearchBoxWrapper = styled.div`
display: flex; display: flex;
@ -15,21 +18,31 @@ export const WidgetsMainContainer = styled.div`
user-select: none; user-select: none;
`; `;
export const WidgetsOuterContainer = styled.div` const StyledWidgetsOuterContainer = styled.div`
flex: 1 1 0; flex: 1 1 0;
overflow: hidden; overflow: hidden;
width: ${theme.leftPanel.width}px; width: ${(props) => props.leftPanelWidth}px;
position: relative; position: relative;
flex-direction: column; flex-direction: column;
display: flex; display: flex;
`; `;
export const WidgetsInnerContainer = styled.div` export function WidgetsOuterContainer({ children }) {
const leftPanelWidth = useLeftPanelWidth();
return <StyledWidgetsOuterContainer leftPanelWidth={leftPanelWidth}>{children}</StyledWidgetsOuterContainer>;
}
export const StyledWidgetsInnerContainer = styled.div`
flex: 1 1; flex: 1 1;
overflow: scroll; overflow: scroll;
width: ${theme.leftPanel.width}px; width: ${(props) => props.leftPanelWidth}px;
`; `;
export function WidgetsInnerContainer({ children }) {
const leftPanelWidth = useLeftPanelWidth();
return <StyledWidgetsInnerContainer leftPanelWidth={leftPanelWidth}>{children}</StyledWidgetsInnerContainer>;
}
export const Input = styled.input` export const Input = styled.input`
flex: 1; flex: 1;
min-width: 90px; min-width: 90px;