feat: add split pane for idraw design

This commit is contained in:
chenshenhai 2023-06-03 14:11:04 +08:00
parent b23aeae6f1
commit f643cd7d7b
24 changed files with 1130 additions and 331 deletions

View file

@ -1,149 +0,0 @@
import { createUUID } from '@idraw/util';
import type { DesignData } from '../src';
const data: DesignData = {
components: [
{
uuid: createUUID(),
type: 'component',
name: 'Button default',
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
bgColor: '#1f1f1f',
children: [
{
uuid: createUUID(),
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: createUUID(),
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
},
{
uuid: createUUID(),
type: 'component',
name: 'Button primary',
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
bgColor: '#f0f0f0',
children: [
{
uuid: createUUID(),
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: createUUID(),
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
}
],
modules: [],
pages: []
};
export default data;

View file

@ -0,0 +1,91 @@
import { createUUID } from '@idraw/util';
import type { DesignComponent, DesignComponentItem } from '../../../src';
function createButtonItem(variantName: string) {
const componentItem: DesignComponentItem = {
uuid: createUUID(),
type: 'component-item',
name: `Button ${variantName}`,
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
children: [
{
uuid: createUUID(),
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: createUUID(),
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: createUUID(),
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
};
return componentItem;
}
export function createButton(name: string) {
const button: DesignComponent = {
uuid: createUUID(),
type: 'component',
name: `Button ${name}`,
x: 50,
y: 50,
w: 800,
h: 400,
desc: {
default: createButtonItem('default'),
variants: [createButtonItem('primary'), createButtonItem('secondary')]
}
};
return button;
}

View file

@ -0,0 +1,91 @@
import { createUUID } from '@idraw/util';
import type { DesignComponent, DesignComponentItem } from '../../../src';
function createCheckboxItem(variantName: string) {
const componentItem: DesignComponentItem = {
uuid: createUUID(),
type: 'component-item',
name: `Checkbox ${variantName}`,
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
children: [
{
uuid: createUUID(),
type: 'rect',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: createUUID(),
type: 'rect',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: createUUID(),
type: 'rect',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: createUUID(),
type: 'rect',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: createUUID(),
type: 'rect',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
};
return componentItem;
}
export function createCheckbox(name: string) {
const checkbox: DesignComponent = {
uuid: createUUID(),
type: 'component',
name: `Checkbox ${name}`,
x: 50,
y: 50,
w: 800,
h: 400,
desc: {
default: createCheckboxItem('default'),
variants: [createCheckboxItem('primary'), createCheckboxItem('secondary')]
}
};
return checkbox;
}

View file

@ -0,0 +1,11 @@
import type { DesignData } from '../../src';
import { createButton } from './components/button';
import { createCheckbox } from './components/checkbox';
const data: DesignData = {
components: [createButton('001'), createButton('002'), createCheckbox('001'), createCheckbox('002')],
modules: [],
pages: []
};
export default data;

View file

@ -7,19 +7,19 @@ const dom = document.querySelector('#lab') as HTMLDivElement;
const root = createRoot(dom);
const App = () => {
// const style = { margin: 0, padding: 0 }
// const [width, setWidth] = useState<number>(window.innerWidth);
// const [height, setHeight] = useState<number>(window.innerHeight);
// useEffect(() => {
// window.addEventListener('resize', () => {
// setWidth(window.innerWidth);
// setHeight(window.innerHeight);
// });
// }, []);
const style = { margin: 0, padding: 0 };
const [width, setWidth] = useState<number>(window.innerWidth);
const [height, setHeight] = useState<number>(window.innerHeight);
useEffect(() => {
window.addEventListener('resize', () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
});
}, []);
const style = { margin: 40 };
const width = 800;
const height = 600;
// const style = { margin: 40 };
// const width = 800;
// const height = 600;
return <Design width={width} height={height} style={style} designData={data} />;
};

View file

@ -1,28 +1,6 @@
import { createContext } from 'react';
import type { Dispatch } from 'react';
import type { Data } from '@idraw/types';
import { DesignData } from './types';
export interface DesignState {
designData: DesignData | null;
viewDrawData: Data | null;
viewDrawUUID: string | null;
themeMode: 'light' | 'dark';
}
export type DesignActionType = 'updateThemeMode' | 'updateDesignData';
export type DesignAction = {
type: DesignActionType;
payload: Partial<DesignState>;
};
export type DesignDispatch = Dispatch<DesignAction>;
export interface DesignContext {
state?: DesignState;
dispatch?: DesignDispatch;
}
import { DesignData, DesignState, DesignAction, DesignContext } from './types';
import { parseComponentsToDrawData } from './util/view-data';
export function createDesignData(): DesignData {
return {
@ -32,6 +10,16 @@ export function createDesignData(): DesignData {
};
}
export function createDesignContextState(opts?: Partial<DesignState>): DesignState {
return {
designData: opts?.designData || createDesignData(),
activeDrawDataType: 'component', // TODO
themeMode: opts?.themeMode || 'light',
viewDrawData: null,
viewDrawUUID: null
};
}
export function createDesignReducer(state: DesignState, action: DesignAction): DesignState {
switch (action.type) {
case 'updateThemeMode': {
@ -52,24 +40,40 @@ export function createDesignReducer(state: DesignState, action: DesignAction): D
return {
...state,
...{
data: action?.payload?.designData
designData: action?.payload?.designData
}
};
}
case 'switchDrawDataType': {
if (!action?.payload?.activeDrawDataType) {
return state;
}
let newState = {
...state,
...{
activeDrawDataType: action?.payload.activeDrawDataType
}
};
if (action.payload.activeDrawDataType === 'component') {
newState = {
...newState,
viewDrawData: parseComponentsToDrawData(state.designData?.components || [])
};
}
return newState;
}
default:
return state;
}
}
export function createDesignContextState(opts?: Partial<DesignState>): DesignState {
return {
designData: opts?.designData || createDesignData(),
themeMode: opts?.themeMode || 'light',
viewDrawData: null,
viewDrawUUID: null
};
}
export const Context = createContext<DesignContext>({});
export const Context = createContext<DesignContext>({
state: createDesignContextState(),
dispatch: () => {
return;
}
});
export const Provider = Context.Provider;

View file

@ -1,9 +1,10 @@
@import "./theme/dark.less";
@import "./theme/light.less";
@import './theme/dark.less';
@import './theme/light.less';
@import "./icons/index.less";
@import './icons/index.less';
@import "./modules/header.less";
@import "./modules/sketch.less";
@import "./modules/toolbar.less";
@import "./modules/panel-layer.less";
@import './modules/header.less';
@import './modules/sketch.less';
@import './modules/toolbar.less';
@import './modules/panel-layer.less';
@import './modules/split-pane.less';

View file

@ -10,28 +10,18 @@
.@{mod-panel-layer}-header {
height: 40px;
box-sizing: border-box;
// border-bottom: 1px solid;
// border-color: ~'var(--@{prefix}-border-color)';
}
.@{mod-panel-layer}-content {
flex: 1;
// display: flex;
width: 100%;
}
.@{mod-panel-layer}-footer {
height: 40px;
}
.@{mod-panel-layer}-tabs {
// width: 100%;
// display: flex;
// padding-bottom: 100px;
.@{mod-panel-layer}-tab-title {
font-size: 16;
margin: 0 4px;
}
overflow: auto;
// fix antd style
.ant-tree-switcher {
display: flex;
justify-content: center;
@ -44,4 +34,28 @@
text-overflow: ellipsis;
}
}
.@{mod-panel-layer}-footer {
height: 32px;
box-sizing: border-box;
border-top: 1px solid;
border-color: ~'var(--@{prefix}-border-color)';
}
.@{mod-panel-layer}-tabs {
// width: 100%;
// display: flex;
// padding-bottom: 100px;
.@{mod-panel-layer}-tab-title {
font-size: 16;
margin: 0 4px;
}
.@{mod-panel-layer}-tab-content {
flex: 1;
width: 100%;
height: 100%;
overflow: auto;
}
}
}

View file

@ -47,4 +47,19 @@
left: 0;
right: 0;
}
.@{mod-sketch}-left {
border-right: 1px solid;
border-color: ~'var(--@{prefix}-border-color)';
height: 100%;
}
.@{mod-sketch}-right {
border-left: 1px solid;
border-color: ~'var(--@{prefix}-border-color)';
height: 100%;
}
.@{mod-sketch}-center {
display: block;
}
}

View file

@ -0,0 +1,55 @@
// @import '../variable.less';
@mod-split-pane: ~'@{prefix}-mod-split-pane';
.@{mod-split-pane} {
// background: #000;
opacity: 0.2;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
&:hover {
-webkit-transition: all 2s ease;
transition: all 2s ease;
}
&.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
&.horizontal:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
&.vertical {
width: 11px;
margin: 0 -6px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
}
&.vertical:hover {
border-left: 5px solid rgba(0, 0, 0, 0.5);
border-right: 5px solid rgba(0, 0, 0, 0.5);
}
&.disabled {
cursor: not-allowed;
}
&.disabled:hover {
border-color: transparent;
}
}

View file

@ -1,41 +1,28 @@
import React from 'react';
import React, { useContext } from 'react';
import type { CSSProperties } from 'react';
import classnames from 'classnames';
import Tabs from 'antd/es/tabs';
import type { TabsProps } from 'antd';
import { prefixName } from './config';
import { LayerTree } from './layer-tree';
import FileOutlined from '@ant-design/icons/FileOutlined';
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
import CalculatorOutlined from '@ant-design/icons/CalculatorOutlined';
import { prefixName } from './config';
import { LayerTree } from './layer-tree';
import { Context } from '../../context';
import { DesignDrawDataType } from '../../types';
const items: TabsProps['items'] = [
{
key: 'page',
label: <FileOutlined className={prefixName('tab', 'title')} />,
children: (
<div style={{ width: '100%', overflow: 'auto' }}>
<LayerTree type="page" />
</div>
)
label: <FileOutlined className={prefixName('tab', 'title')} />
},
{
key: 'module',
label: <AppstoreOutlined className={prefixName('tab', 'title')} />,
children: (
<div style={{ width: '100%', overflow: 'auto' }}>
<LayerTree type="module" />
</div>
)
label: <AppstoreOutlined className={prefixName('tab', 'title')} />
},
{
key: 'component',
label: <CalculatorOutlined className={prefixName('tab', 'title')} />,
children: (
<div style={{ width: '100%', overflow: 'auto' }}>
<LayerTree type="component" />
</div>
)
label: <CalculatorOutlined className={prefixName('tab', 'title')} />
}
];
@ -46,15 +33,27 @@ export interface PanelLayerProps {
export const PanelLayer = (props: PanelLayerProps) => {
const { className, style } = props;
const { state, dispatch } = useContext(Context);
const defaultTabKey = items[2].key;
return (
<div style={style} className={classnames(prefixName(), className)}>
{/* <div className={prefixName('header')}>header</div> */}
<div className={prefixName('content')}>
<Tabs className={prefixName('tabs')} defaultActiveKey={defaultTabKey} centered items={items} size="small" />
<div className={prefixName('header')}>
<Tabs
className={prefixName('tabs')}
tabBarStyle={{ marginBottom: 0 }}
activeKey={state?.activeDrawDataType as string}
centered
items={items}
size="small"
onTabClick={(activeKey: string) => {
dispatch({ type: 'switchDrawDataType', payload: { activeDrawDataType: activeKey as DesignDrawDataType } });
}}
/>
</div>
{/* <div className={prefixName('footer')}>footer</div> */}
<div className={prefixName('content')}>
<LayerTree type={state.activeDrawDataType} />
</div>
<div className={prefixName('footer')}>footer</div>
</div>
);
};

View file

@ -8,7 +8,7 @@ import { parseComponentViewTree } from '../../util/component';
import type { CSSProperties } from 'react';
import type { DataNode, TreeProps } from 'antd/es/tree';
import type { DesignItemType } from '../../types';
import type { DesignDrawDataType } from '../../types';
const { DirectoryTree } = Tree;
const baseName = 'layer-tree';
@ -16,7 +16,7 @@ const baseName = 'layer-tree';
export interface LayerTreeProps {
className?: string;
style?: CSSProperties;
type: DesignItemType;
type: DesignDrawDataType;
}
export const LayerTree = (props: LayerTreeProps) => {
@ -24,18 +24,18 @@ export const LayerTree = (props: LayerTreeProps) => {
const { state } = useContext(Context);
const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
// TODO
console.log('selected', selectedKeys, info);
};
let treeData: DataNode[] = [];
if (type === 'component') {
treeData = parseComponentViewTree(state?.designData || null);
}
return (
<div style={style} className={classnames(prefixName(baseName), className)}>
<DirectoryTree showLine blockNode switcherIcon={<DownOutlined />} icon={null} defaultExpandedKeys={['0-0-0']} onSelect={onSelect} treeData={treeData} />
<DirectoryTree showLine blockNode switcherIcon={<DownOutlined />} icon={null} onSelect={onSelect} treeData={treeData} />
</div>
);
};

View file

@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
import classnames from 'classnames';
import { Core, MiddlewareScroller, MiddlewareSelector } from '@idraw/core';
import { calcElementsContextSize } from '@idraw/util';
import Drawer from 'antd/es/drawer';
import { getData } from '../../data';
import { Toolbar } from '../toolbar';
import { PanelLayer } from '../panel-layer';
@ -10,9 +9,11 @@ import { Header } from '../header';
import type { CSSProperties } from 'react';
import { createPrefixName } from '../../css';
import { HEADER_HEIGHT } from './layout';
import SplitPane from '../split-pane';
const modName = 'mod-sketch';
const siderWidth = 200;
const leftSiderDefaultWidth = 240;
const rightSiderDefaultWidth = 200;
const prefixName = createPrefixName(modName);
@ -26,8 +27,6 @@ export interface SketchProps {
export const Sketch = (props: SketchProps) => {
const ref = useRef<HTMLDivElement>(null);
const refCore = useRef<Core | null>(null);
const refLeftDOM = useRef<HTMLDivElement | null>(null);
const refRighttDOM = useRef<HTMLDivElement | null>(null);
const { className, style, width, height } = props;
const data = getData();
const devicePixelRatio = window.devicePixelRatio;
@ -35,11 +34,15 @@ export const Sketch = (props: SketchProps) => {
const [openLeftSider, setOpenLeftSider] = useState<boolean>(true);
const [openRightSider, setOpenRightSider] = useState<boolean>(false);
const [leftWidth, setLeftWidth] = useState<number>(openLeftSider ? leftSiderDefaultWidth : 0);
const [rightWidth, setRightWidth] = useState<number>(openRightSider ? rightSiderDefaultWidth : 0);
const [centerWidth, setCenterWidth] = useState<number>(width - leftWidth - rightWidth);
useEffect(() => {
if (ref?.current) {
if (!refCore?.current) {
const options = {
width,
width: centerWidth,
height: height - HEADER_HEIGHT,
devicePixelRatio
};
@ -52,90 +55,98 @@ export const Sketch = (props: SketchProps) => {
}
}, []);
useEffect(() => {
const prevWidth = leftWidth + centerWidth + rightWidth;
let newLeftWidth = Math.floor(width * (leftWidth / prevWidth));
let newRightWidth = Math.floor(width * (rightWidth / prevWidth));
newLeftWidth = Math.min(newLeftWidth, leftSiderDefaultWidth);
newRightWidth = Math.min(newRightWidth, rightSiderDefaultWidth);
const newCenterWidth = width - newLeftWidth - newRightWidth;
setLeftWidth(newLeftWidth);
setRightWidth(newRightWidth);
setCenterWidth(newCenterWidth);
}, [height, width]);
useEffect(() => {
if (!refCore?.current) {
return;
}
const core = refCore.current;
const contextSize = calcElementsContextSize(data.elements, { viewWidth: width, viewHeight: height });
core.resize({
width,
width: centerWidth,
height: height - HEADER_HEIGHT,
devicePixelRatio,
...contextSize
});
}, [height, width]);
}, [height, centerWidth]);
return (
<div className={classnames(prefixName(), className)} style={{ ...style, ...{ width, height, padding: 0 } }}>
<div className={prefixName('header')} style={{ height: HEADER_HEIGHT }}>
<Header />
</div>
<div ref={ref} className={prefixName('content')} style={{ top: HEADER_HEIGHT }}></div>
<div ref={refLeftDOM}></div>
<div ref={refRighttDOM}></div>
<div className={prefixName('content')} style={{ top: HEADER_HEIGHT }}>
<SplitPane
split="vertical"
defaultSize={centerWidth + rightWidth}
allowResize
onChange={(px: number) => {
setCenterWidth(px - rightWidth);
setLeftWidth(width - px);
}}
pane1Style={{
width: leftWidth
}}
pane2Style={{
width: centerWidth + rightWidth
}}
>
<div>
<PanelLayer className={prefixName('left')} />
</div>
<div style={{ width: centerWidth + rightWidth, display: 'flex', flexDirection: 'row' }}>
<div ref={ref} className={prefixName('center')} style={{ width: centerWidth, height: height - HEADER_HEIGHT }}></div>
<div className={prefixName('right')} style={{ width: rightWidth, height: height - HEADER_HEIGHT }}>
Right
</div>
</div>
</SplitPane>
</div>
<Toolbar
className={prefixName('toolbar-position')}
openLeftSider={openLeftSider}
openRightSider={openRightSider}
onClickToggleLayer={() => {
setOpenLeftSider(openLeftSider ? false : true);
const open = openLeftSider ? false : true;
let newLeftWidth = leftWidth;
if (open) {
newLeftWidth = leftSiderDefaultWidth;
} else {
newLeftWidth = 0;
}
setLeftWidth(newLeftWidth);
setCenterWidth(width - newLeftWidth - rightWidth);
setRightWidth(rightWidth);
setOpenLeftSider(open);
}}
onClickToggleSetting={() => {
setOpenRightSider(openRightSider ? false : true);
const open = openRightSider ? false : true;
let newRightWidth = rightWidth;
if (open) {
newRightWidth = rightSiderDefaultWidth;
} else {
newRightWidth = 0;
}
setLeftWidth(leftWidth);
setCenterWidth(width - leftWidth - newRightWidth);
setRightWidth(newRightWidth);
setOpenRightSider(open);
}}
/>
<Drawer
// title="left Drawer"
placement="left"
closable={false}
onClose={() => {
console.log('on close left');
setOpenLeftSider(false);
}}
mask={false}
open={openLeftSider}
getContainer={() => {
return refLeftDOM.current as HTMLDivElement;
}}
width={siderWidth}
bodyStyle={{
padding: 0
}}
rootStyle={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0
}}
>
<PanelLayer />
</Drawer>
<Drawer
title="right Drawer"
placement="right"
onClose={() => {
console.log('on close right');
setOpenRightSider(false);
}}
mask={false}
open={openRightSider}
getContainer={() => {
return refRighttDOM.current as HTMLDivElement;
}}
width={siderWidth}
rootStyle={{
position: 'absolute',
right: 0,
bottom: 0,
left: 0
}}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
</div>
);
};

View file

@ -0,0 +1,6 @@
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/index.js
import SplitPane from './split-pane';
import Pane from './pane';
export default SplitPane;
export { Pane };

View file

@ -0,0 +1,49 @@
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/Pane.js
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import React from 'react';
class Pane extends React.PureComponent {
render() {
const { children, className, split, style: styleProps, size, eleRef } = this.props;
const classes = ['Pane', split, className];
let style = {
flex: 1,
position: 'relative',
outline: 'none'
};
if (size !== undefined) {
if (split === 'vertical') {
style.width = size;
} else {
style.height = size;
style.display = 'flex';
}
style.flex = 'none';
}
style = Object.assign({}, style, styleProps || {});
return (
<div ref={eleRef} className={classes.join(' ')} style={style}>
{children}
</div>
);
}
}
// Pane.propTypes = {
// className: PropTypes.string.isRequired,
// children: PropTypes.node.isRequired,
// size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// split: PropTypes.oneOf(['vertical', 'horizontal']),
// style: stylePropType,
// eleRef: PropTypes.func,
// };
// Pane.defaultProps = {};
export default Pane;

View file

@ -0,0 +1,64 @@
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/Resizer.js
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import React from 'react';
import { createPrefixName } from '../../css';
const modName = 'mod-split-pane';
const prefixName = createPrefixName(modName);
export const RESIZER_DEFAULT_CLASSNAME = prefixName();
class Resizer extends React.Component {
render() {
const { className, onClick, onDoubleClick, onMouseDown, onTouchEnd, onTouchStart, resizerClassName = RESIZER_DEFAULT_CLASSNAME, split, style } = this.props;
const classes = [resizerClassName, split, className];
return (
<span
role="presentation"
className={classes.join(' ')}
style={style}
onMouseDown={(event) => onMouseDown(event)}
onTouchStart={(event) => {
event.preventDefault();
onTouchStart(event);
}}
onTouchEnd={(event) => {
event.preventDefault();
onTouchEnd(event);
}}
onClick={(event) => {
if (onClick) {
event.preventDefault();
onClick(event);
}
}}
onDoubleClick={(event) => {
if (onDoubleClick) {
event.preventDefault();
onDoubleClick(event);
}
}}
/>
);
}
}
// Resizer.propTypes = {
// className: PropTypes.string.isRequired,
// onClick: PropTypes.func,
// onDoubleClick: PropTypes.func,
// onMouseDown: PropTypes.func.isRequired,
// onTouchStart: PropTypes.func.isRequired,
// onTouchEnd: PropTypes.func.isRequired,
// split: PropTypes.oneOf(['vertical', 'horizontal']),
// style: stylePropType,
// resizerClassName: PropTypes.string.isRequired,
// };
// Resizer.defaultProps = {
// resizerClassName: RESIZER_DEFAULT_CLASSNAME,
// };
export default Resizer;

View file

@ -0,0 +1,374 @@
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/SplitPane.js
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import React from 'react';
// import PropTypes from 'prop-types';
// import stylePropType from 'react-style-proptype';
// import { polyfill } from 'react-lifecycles-compat';
import Pane from './pane';
import Resizer, { RESIZER_DEFAULT_CLASSNAME } from './resizer';
function unFocus(document, window) {
if (document.selection) {
document.selection.empty();
} else {
try {
window.getSelection().removeAllRanges();
// eslint-disable-next-line no-empty
} catch (e) {}
}
}
function getDefaultSize(defaultSize, minSize, maxSize, draggedSize) {
if (typeof draggedSize === 'number') {
const min = typeof minSize === 'number' ? minSize : 0;
const max = typeof maxSize === 'number' && maxSize >= 0 ? maxSize : Infinity;
return Math.max(min, Math.min(max, draggedSize));
}
if (defaultSize !== undefined) {
return defaultSize;
}
return minSize;
}
function removeNullChildren(children) {
return React.Children.toArray(children).filter((c) => c);
}
class SplitPane extends React.Component<any, any> {
constructor(props) {
super(props);
this.onMouseDown = this.onMouseDown.bind(this);
this.onTouchStart = this.onTouchStart.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
// order of setting panel sizes.
// 1. size
// 2. getDefaultSize(defaultSize, minsize, maxSize)
const { size, defaultSize, minSize, maxSize, primary } = props;
const initialSize = size !== undefined ? size : getDefaultSize(defaultSize, minSize, maxSize, null);
this.state = {
active: false,
resized: false,
pane1Size: primary === 'first' ? initialSize : undefined,
pane2Size: primary === 'second' ? initialSize : undefined,
// these are props that are needed in static functions. ie: gDSFP
instanceProps: {
size
}
};
}
componentDidMount() {
document.addEventListener('mouseup', this.onMouseUp);
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('touchmove', this.onTouchMove);
this.setState(SplitPane.getSizeUpdate(this.props, this.state));
}
static getDerivedStateFromProps(nextProps, prevState) {
return SplitPane.getSizeUpdate(nextProps, prevState);
}
componentWillUnmount() {
document.removeEventListener('mouseup', this.onMouseUp);
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('touchmove', this.onTouchMove);
}
onMouseDown(event) {
const eventWithTouches = Object.assign({}, event, {
touches: [{ clientX: event.clientX, clientY: event.clientY }]
});
this.onTouchStart(eventWithTouches);
}
onTouchStart(event) {
const { allowResize, onDragStarted, split } = this.props;
if (allowResize) {
unFocus(document, window);
const position = split === 'vertical' ? event.touches[0].clientX : event.touches[0].clientY;
if (typeof onDragStarted === 'function') {
onDragStarted();
}
this.setState({
active: true,
position
});
}
}
onMouseMove(event) {
const eventWithTouches = Object.assign({}, event, {
touches: [{ clientX: event.clientX, clientY: event.clientY }]
});
this.onTouchMove(eventWithTouches);
}
onTouchMove(event) {
const { allowResize, maxSize, minSize, onChange, split, step } = this.props;
const { active, position } = this.state;
if (allowResize && active) {
unFocus(document, window);
const isPrimaryFirst = this.props.primary === 'first';
const ref = isPrimaryFirst ? this.pane1 : this.pane2;
const ref2 = isPrimaryFirst ? this.pane2 : this.pane1;
if (ref) {
const node = ref;
const node2 = ref2;
if (node.getBoundingClientRect) {
const width = node.getBoundingClientRect().width;
const height = node.getBoundingClientRect().height;
const current = split === 'vertical' ? event.touches[0].clientX : event.touches[0].clientY;
const size = split === 'vertical' ? width : height;
let positionDelta = position - current;
if (step) {
if (Math.abs(positionDelta) < step) {
return;
}
// Integer division
// eslint-disable-next-line no-bitwise
positionDelta = ~~(positionDelta / step) * step;
}
let sizeDelta = isPrimaryFirst ? positionDelta : -positionDelta;
const pane1Order = parseInt(window.getComputedStyle(node).order);
const pane2Order = parseInt(window.getComputedStyle(node2).order);
if (pane1Order > pane2Order) {
sizeDelta = -sizeDelta;
}
let newMaxSize = maxSize;
if (maxSize !== undefined && maxSize <= 0) {
const splitPane = this.splitPane;
if (split === 'vertical') {
newMaxSize = splitPane.getBoundingClientRect().width + maxSize;
} else {
newMaxSize = splitPane.getBoundingClientRect().height + maxSize;
}
}
let newSize = size - sizeDelta;
const newPosition = position - positionDelta;
if (newSize < minSize) {
newSize = minSize;
} else if (maxSize !== undefined && newSize > newMaxSize) {
newSize = newMaxSize;
} else {
this.setState({
position: newPosition,
resized: true
});
}
if (onChange) onChange(newSize);
this.setState({
draggedSize: newSize,
[isPrimaryFirst ? 'pane1Size' : 'pane2Size']: newSize
});
}
}
}
}
onMouseUp() {
const { allowResize, onDragFinished } = this.props;
const { active, draggedSize } = this.state;
if (allowResize && active) {
if (typeof onDragFinished === 'function') {
onDragFinished(draggedSize);
}
this.setState({ active: false });
}
}
// we have to check values since gDSFP is called on every render and more in StrictMode
static getSizeUpdate(props, state) {
const newState = {};
const { instanceProps } = state;
if (instanceProps.size === props.size && props.size !== undefined) {
return {};
}
const newSize = props.size !== undefined ? props.size : getDefaultSize(props.defaultSize, props.minSize, props.maxSize, state.draggedSize);
if (props.size !== undefined) {
newState.draggedSize = newSize;
}
const isPanel1Primary = props.primary === 'first';
newState[isPanel1Primary ? 'pane1Size' : 'pane2Size'] = newSize;
newState[isPanel1Primary ? 'pane2Size' : 'pane1Size'] = undefined;
newState.instanceProps = { size: props.size };
return newState;
}
render() {
const {
allowResize,
children,
className,
onResizerClick,
onResizerDoubleClick,
paneClassName,
pane1ClassName,
pane2ClassName,
paneStyle,
pane1Style: pane1StyleProps,
pane2Style: pane2StyleProps,
resizerClassName,
resizerStyle,
split,
style: styleProps
} = this.props;
const { pane1Size, pane2Size } = this.state;
const disabledClass = allowResize ? '' : 'disabled';
const resizerClassNamesIncludingDefault = resizerClassName ? `${resizerClassName} ${RESIZER_DEFAULT_CLASSNAME}` : resizerClassName;
const notNullChildren = removeNullChildren(children);
const style = {
display: 'flex',
flex: 1,
height: '100%',
position: 'absolute',
outline: 'none',
overflow: 'hidden',
MozUserSelect: 'text',
WebkitUserSelect: 'text',
msUserSelect: 'text',
userSelect: 'text',
...styleProps
};
if (split === 'vertical') {
Object.assign(style, {
flexDirection: 'row',
left: 0,
right: 0
});
} else {
Object.assign(style, {
bottom: 0,
flexDirection: 'column',
minHeight: '100%',
top: 0,
width: '100%'
});
}
const classes = ['SplitPane', className, split, disabledClass];
const pane1Style = { ...paneStyle, ...pane1StyleProps };
const pane2Style = { ...paneStyle, ...pane2StyleProps };
const pane1Classes = ['Pane1', paneClassName, pane1ClassName].join(' ');
const pane2Classes = ['Pane2', paneClassName, pane2ClassName].join(' ');
return (
<div
className={classes.join(' ')}
ref={(node) => {
this.splitPane = node;
}}
style={style}
>
<Pane
className={pane1Classes}
key="pane1"
eleRef={(node) => {
this.pane1 = node;
}}
size={pane1Size}
split={split}
style={pane1Style}
>
{notNullChildren[0]}
</Pane>
<Resizer
className={disabledClass}
onClick={onResizerClick}
onDoubleClick={onResizerDoubleClick}
onMouseDown={this.onMouseDown}
onTouchStart={this.onTouchStart}
onTouchEnd={this.onMouseUp}
key="resizer"
resizerClassName={resizerClassNamesIncludingDefault}
split={split}
style={resizerStyle || {}}
/>
<Pane
className={pane2Classes}
key="pane2"
eleRef={(node) => {
this.pane2 = node;
}}
size={pane2Size}
split={split}
style={pane2Style}
>
{notNullChildren[1]}
</Pane>
</div>
);
}
}
// SplitPane.propTypes = {
// allowResize: PropTypes.bool,
// children: PropTypes.arrayOf(PropTypes.node).isRequired,
// className: PropTypes.string,
// primary: PropTypes.oneOf(['first', 'second']),
// minSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// maxSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// defaultSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// split: PropTypes.oneOf(['vertical', 'horizontal']),
// onDragStarted: PropTypes.func,
// onDragFinished: PropTypes.func,
// onChange: PropTypes.func,
// onResizerClick: PropTypes.func,
// onResizerDoubleClick: PropTypes.func,
// style: stylePropType,
// resizerStyle: stylePropType,
// paneClassName: PropTypes.string,
// pane1ClassName: PropTypes.string,
// pane2ClassName: PropTypes.string,
// paneStyle: stylePropType,
// pane1Style: stylePropType,
// pane2Style: stylePropType,
// resizerClassName: PropTypes.string,
// step: PropTypes.number
// };
// SplitPane.defaultProps = {
// allowResize: true,
// minSize: 50,
// primary: 'first',
// split: 'vertical',
// paneClassName: '',
// pane1ClassName: '',
// pane2ClassName: ''
// };
// polyfill(SplitPane);
export default SplitPane;

View file

@ -0,0 +1,25 @@
import type { Dispatch } from 'react';
import type { Data } from '@idraw/types';
import { DesignData, DesignDrawDataType } from './data';
export interface DesignState {
activeDrawDataType: DesignDrawDataType;
designData: DesignData | null;
viewDrawData: Data | null;
viewDrawUUID: string | null;
themeMode: 'light' | 'dark';
}
export type DesignActionType = 'updateThemeMode' | 'updateDesignData' | 'switchDrawDataType';
export type DesignAction = {
type: DesignActionType;
payload: Partial<DesignState>;
};
export type DesignDispatch = Dispatch<DesignAction>;
export interface DesignContext {
state: DesignState;
dispatch: DesignDispatch;
}

View file

@ -1,13 +1,23 @@
import type { Element, ElementType, ElementSize, ElementBaseDesc } from '@idraw/types';
export type DesignItemType = 'component' | 'module' | 'page';
export type DesignItemType = 'component' | 'component-item' | 'module' | 'page';
export type DesignComponentItem = Omit<ElementSize, 'angle'> & {
uuid: string;
type: 'component-item';
name: string;
desc?: ElementBaseDesc & {
children: Array<Element<ElementType> | DesignComponentItem>;
};
};
export type DesignComponent = Omit<ElementSize, 'angle'> & {
uuid: string;
type: 'component';
name: string;
desc?: ElementBaseDesc & {
children: Array<Element<ElementType> | DesignComponent>;
default: DesignComponentItem;
variants: DesignComponentItem[];
};
};

View file

@ -1,2 +1,3 @@
export * from './data';
export * from './context';
export * from './view';

View file

@ -1,5 +1,9 @@
import type { ElementType } from '@idraw/types';
import type { DesignItemType } from './data';
export interface ViewTreeNode {
title: string;
key: string;
type: DesignItemType | ElementType;
children?: ViewTreeNode[];
}

View file

@ -1,24 +1,11 @@
import { Data } from '@idraw/types';
import { ViewTreeNode, DesignData, DesignComponent } from '../types';
import { parseComponentToViewTreeNode } from './view-tree';
export function parseComponentViewTree(designData: DesignData | null): ViewTreeNode[] {
const list: ViewTreeNode[] = [];
const treeNodes: ViewTreeNode[] = [];
designData?.components?.forEach((comp: DesignComponent) => {
const children: ViewTreeNode[] = [];
if (Array.isArray(comp?.desc?.children)) {
comp?.desc?.children?.forEach((child) => {
children.push({
key: child.uuid,
title: child.name || 'Unamed',
children: []
});
});
}
list.push({
key: comp.uuid,
title: comp.name || 'Unamed',
children
});
const node = parseComponentToViewTreeNode(comp);
treeNodes.push(node);
});
return list;
return treeNodes;
}

View file

@ -0,0 +1,66 @@
import { deepClone } from '@idraw/util';
import type { Data, Element } from '@idraw/types';
import type { DesignComponent, DesignComponentItem } from '../types';
function parseComponentItemToElement(item: DesignComponentItem): Element<'group'> {
const elem: Element<'group'> = {
uuid: item.uuid,
name: item.name,
type: 'group',
x: item.x,
y: item.y,
w: item.w,
h: item.h,
desc: {
...item.desc,
children: []
}
};
item.desc?.children?.forEach?.((child) => {
if (child.type === 'component-item') {
const childElem = parseComponentItemToElement(child);
elem.desc.children.push(childElem);
} else {
const childElem = deepClone(child);
elem.desc.children.push(childElem);
}
});
return elem;
}
function parseComponentToElement(comp: DesignComponent): Element<'group'> {
const elem: Element<'group'> = {
uuid: comp.uuid,
name: comp.name,
type: 'group',
x: comp.x,
y: comp.y,
w: comp.w,
h: comp.h,
desc: {
children: []
}
};
if (comp?.desc?.default) {
elem.desc.children.push(parseComponentItemToElement(comp.desc.default));
}
if (comp?.desc?.variants && Array.isArray(comp?.desc?.variants)) {
comp.desc.variants.forEach((item) => {
elem.desc.children.push(parseComponentItemToElement(item));
});
}
return elem;
}
export function parseComponentsToDrawData(components: DesignComponent[]): Data {
const data: Data = {
elements: []
};
components.forEach((comp: DesignComponent) => {
const elem = parseComponentToElement(comp);
data.elements.push(elem);
});
return data;
}

View file

@ -0,0 +1,70 @@
import type { Element, ElementType } from '@idraw/types';
import { ViewTreeNode, DesignComponent, DesignComponentItem } from '../types';
function parseElementToViewTreeNode(elem: Element<ElementType>): ViewTreeNode | null {
let treeNode: ViewTreeNode | null = null;
if (elem.uuid) {
treeNode = {
key: elem.uuid,
title: elem.name || 'Unamed',
type: elem.type,
children: []
};
if (Array.isArray((elem as Element<'group'>)?.desc?.children)) {
(elem as Element<'group'>).desc.children.forEach((child: Element<ElementType>) => {
const childNode = parseElementToViewTreeNode(child);
if (childNode) {
treeNode?.children?.push(childNode);
}
});
}
}
return treeNode;
}
function parseComponentItemToViewTreeNode(comp: DesignComponentItem): ViewTreeNode {
const treeNode: Required<ViewTreeNode> = {
key: comp.uuid,
title: comp.name || 'Unamed',
type: comp.type,
children: []
};
if (comp?.desc?.children && Array.isArray(comp?.desc?.children)) {
comp.desc.children.forEach((child) => {
let childNode: ViewTreeNode | null = null;
if (child.type === 'component') {
childNode = parseComponentToViewTreeNode(child as DesignComponent);
} else {
childNode = parseElementToViewTreeNode(child as Element<ElementType>);
}
if (childNode) {
treeNode.children.push(childNode);
}
});
}
return treeNode;
}
export function parseComponentToViewTreeNode(comp: DesignComponent): ViewTreeNode {
const treeNode: Required<ViewTreeNode> = {
key: comp.uuid,
title: comp.name || 'Unamed',
type: comp.type,
children: []
};
if (comp?.desc?.default) {
const node = parseComponentItemToViewTreeNode(comp.desc.default);
treeNode.children.push(node);
}
if (Array.isArray(comp?.desc?.variants)) {
comp?.desc?.variants?.forEach((child: DesignComponentItem) => {
const node = parseComponentItemToViewTreeNode(child);
treeNode.children.push(node);
});
}
return treeNode;
}