From f643cd7d7b06c15f03d0eeff1d0e345426d7edd7 Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sat, 3 Jun 2023 14:11:04 +0800 Subject: [PATCH] feat: add split pane for idraw design --- packages/design/dev/data.ts | 149 ------- packages/design/dev/data/components/button.ts | 91 +++++ .../design/dev/data/components/checkbox.ts | 91 +++++ packages/design/dev/data/index.ts | 11 + packages/design/dev/main.tsx | 24 +- packages/design/src/context.ts | 74 ++-- packages/design/src/css/index.less | 15 +- .../design/src/css/modules/panel-layer.less | 44 ++- packages/design/src/css/modules/sketch.less | 15 + .../design/src/css/modules/split-pane.less | 55 +++ .../design/src/modules/panel-layer/index.tsx | 51 ++- .../src/modules/panel-layer/layer-tree.tsx | 8 +- packages/design/src/modules/sketch/index.tsx | 137 ++++--- .../design/src/modules/split-pane/index.tsx | 6 + .../design/src/modules/split-pane/pane.tsx | 49 +++ .../design/src/modules/split-pane/resizer.tsx | 64 +++ .../src/modules/split-pane/split-pane.tsx | 374 ++++++++++++++++++ packages/design/src/types/context.ts | 25 ++ packages/design/src/types/data.ts | 14 +- packages/design/src/types/index.ts | 1 + packages/design/src/types/view.ts | 4 + packages/design/src/util/component.ts | 23 +- packages/design/src/util/view-data.ts | 66 ++++ packages/design/src/util/view-tree.ts | 70 ++++ 24 files changed, 1130 insertions(+), 331 deletions(-) delete mode 100644 packages/design/dev/data.ts create mode 100644 packages/design/dev/data/components/button.ts create mode 100644 packages/design/dev/data/components/checkbox.ts create mode 100644 packages/design/dev/data/index.ts create mode 100644 packages/design/src/css/modules/split-pane.less create mode 100644 packages/design/src/modules/split-pane/index.tsx create mode 100644 packages/design/src/modules/split-pane/pane.tsx create mode 100644 packages/design/src/modules/split-pane/resizer.tsx create mode 100644 packages/design/src/modules/split-pane/split-pane.tsx create mode 100644 packages/design/src/types/context.ts create mode 100644 packages/design/src/util/view-data.ts create mode 100644 packages/design/src/util/view-tree.ts diff --git a/packages/design/dev/data.ts b/packages/design/dev/data.ts deleted file mode 100644 index 8954ed3..0000000 --- a/packages/design/dev/data.ts +++ /dev/null @@ -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; diff --git a/packages/design/dev/data/components/button.ts b/packages/design/dev/data/components/button.ts new file mode 100644 index 0000000..4a1c670 --- /dev/null +++ b/packages/design/dev/data/components/button.ts @@ -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; +} diff --git a/packages/design/dev/data/components/checkbox.ts b/packages/design/dev/data/components/checkbox.ts new file mode 100644 index 0000000..95706ec --- /dev/null +++ b/packages/design/dev/data/components/checkbox.ts @@ -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; +} diff --git a/packages/design/dev/data/index.ts b/packages/design/dev/data/index.ts new file mode 100644 index 0000000..aa4c9c4 --- /dev/null +++ b/packages/design/dev/data/index.ts @@ -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; diff --git a/packages/design/dev/main.tsx b/packages/design/dev/main.tsx index 8568b59..685548e 100644 --- a/packages/design/dev/main.tsx +++ b/packages/design/dev/main.tsx @@ -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(window.innerWidth); - // const [height, setHeight] = useState(window.innerHeight); - // useEffect(() => { - // window.addEventListener('resize', () => { - // setWidth(window.innerWidth); - // setHeight(window.innerHeight); - // }); - // }, []); + const style = { margin: 0, padding: 0 }; + const [width, setWidth] = useState(window.innerWidth); + const [height, setHeight] = useState(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 ; }; diff --git a/packages/design/src/context.ts b/packages/design/src/context.ts index 9cf4b10..1b22cc6 100644 --- a/packages/design/src/context.ts +++ b/packages/design/src/context.ts @@ -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; -}; - -export type DesignDispatch = Dispatch; - -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 { + 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 { - return { - designData: opts?.designData || createDesignData(), - themeMode: opts?.themeMode || 'light', - viewDrawData: null, - viewDrawUUID: null - }; -} - -export const Context = createContext({}); +export const Context = createContext({ + state: createDesignContextState(), + dispatch: () => { + return; + } +}); export const Provider = Context.Provider; diff --git a/packages/design/src/css/index.less b/packages/design/src/css/index.less index 3454289..9302367 100644 --- a/packages/design/src/css/index.less +++ b/packages/design/src/css/index.less @@ -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'; diff --git a/packages/design/src/css/modules/panel-layer.less b/packages/design/src/css/modules/panel-layer.less index 9f3a476..c270600 100644 --- a/packages/design/src/css/modules/panel-layer.less +++ b/packages/design/src/css/modules/panel-layer.less @@ -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; + } + } } diff --git a/packages/design/src/css/modules/sketch.less b/packages/design/src/css/modules/sketch.less index 7939831..c43217f 100644 --- a/packages/design/src/css/modules/sketch.less +++ b/packages/design/src/css/modules/sketch.less @@ -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; + } } diff --git a/packages/design/src/css/modules/split-pane.less b/packages/design/src/css/modules/split-pane.less new file mode 100644 index 0000000..d0e90c2 --- /dev/null +++ b/packages/design/src/css/modules/split-pane.less @@ -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; + } +} diff --git a/packages/design/src/modules/panel-layer/index.tsx b/packages/design/src/modules/panel-layer/index.tsx index 11c785d..a09f257 100644 --- a/packages/design/src/modules/panel-layer/index.tsx +++ b/packages/design/src/modules/panel-layer/index.tsx @@ -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: , - children: ( -
- -
- ) + label: }, { key: 'module', - label: , - children: ( -
- -
- ) + label: }, { key: 'component', - label: , - children: ( -
- -
- ) + label: } ]; @@ -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 (
- {/*
header
*/} -
- +
+ { + dispatch({ type: 'switchDrawDataType', payload: { activeDrawDataType: activeKey as DesignDrawDataType } }); + }} + />
- {/*
footer
*/} +
+ +
+
footer
); }; diff --git a/packages/design/src/modules/panel-layer/layer-tree.tsx b/packages/design/src/modules/panel-layer/layer-tree.tsx index ca8baec..93cfb2f 100644 --- a/packages/design/src/modules/panel-layer/layer-tree.tsx +++ b/packages/design/src/modules/panel-layer/layer-tree.tsx @@ -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 (
- } icon={null} defaultExpandedKeys={['0-0-0']} onSelect={onSelect} treeData={treeData} /> + } icon={null} onSelect={onSelect} treeData={treeData} />
); }; diff --git a/packages/design/src/modules/sketch/index.tsx b/packages/design/src/modules/sketch/index.tsx index ede8caa..d8919c8 100644 --- a/packages/design/src/modules/sketch/index.tsx +++ b/packages/design/src/modules/sketch/index.tsx @@ -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(null); const refCore = useRef(null); - const refLeftDOM = useRef(null); - const refRighttDOM = useRef(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(true); const [openRightSider, setOpenRightSider] = useState(false); + const [leftWidth, setLeftWidth] = useState(openLeftSider ? leftSiderDefaultWidth : 0); + const [rightWidth, setRightWidth] = useState(openRightSider ? rightSiderDefaultWidth : 0); + const [centerWidth, setCenterWidth] = useState(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 (
-
-
-
+
+ { + setCenterWidth(px - rightWidth); + setLeftWidth(width - px); + }} + pane1Style={{ + width: leftWidth + }} + pane2Style={{ + width: centerWidth + rightWidth + }} + > +
+ +
+
+
+
+ Right +
+
+
+
{ - 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); }} /> - { - 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 - }} - > - - - { - 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 - }} - > -

Some contents...

-

Some contents...

-

Some contents...

-
); }; diff --git a/packages/design/src/modules/split-pane/index.tsx b/packages/design/src/modules/split-pane/index.tsx new file mode 100644 index 0000000..0298c51 --- /dev/null +++ b/packages/design/src/modules/split-pane/index.tsx @@ -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 }; diff --git a/packages/design/src/modules/split-pane/pane.tsx b/packages/design/src/modules/split-pane/pane.tsx new file mode 100644 index 0000000..424aef6 --- /dev/null +++ b/packages/design/src/modules/split-pane/pane.tsx @@ -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 ( +
+ {children} +
+ ); + } +} + +// 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; diff --git a/packages/design/src/modules/split-pane/resizer.tsx b/packages/design/src/modules/split-pane/resizer.tsx new file mode 100644 index 0000000..8f70b6f --- /dev/null +++ b/packages/design/src/modules/split-pane/resizer.tsx @@ -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 ( + 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; diff --git a/packages/design/src/modules/split-pane/split-pane.tsx b/packages/design/src/modules/split-pane/split-pane.tsx new file mode 100644 index 0000000..a141d4b --- /dev/null +++ b/packages/design/src/modules/split-pane/split-pane.tsx @@ -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 { + 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 ( +
{ + this.splitPane = node; + }} + style={style} + > + { + this.pane1 = node; + }} + size={pane1Size} + split={split} + style={pane1Style} + > + {notNullChildren[0]} + + + { + this.pane2 = node; + }} + size={pane2Size} + split={split} + style={pane2Style} + > + {notNullChildren[1]} + +
+ ); + } +} + +// 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; diff --git a/packages/design/src/types/context.ts b/packages/design/src/types/context.ts new file mode 100644 index 0000000..4d1c7f2 --- /dev/null +++ b/packages/design/src/types/context.ts @@ -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; +}; + +export type DesignDispatch = Dispatch; + +export interface DesignContext { + state: DesignState; + dispatch: DesignDispatch; +} diff --git a/packages/design/src/types/data.ts b/packages/design/src/types/data.ts index f613900..529bb59 100644 --- a/packages/design/src/types/data.ts +++ b/packages/design/src/types/data.ts @@ -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 & { + uuid: string; + type: 'component-item'; + name: string; + desc?: ElementBaseDesc & { + children: Array | DesignComponentItem>; + }; +}; export type DesignComponent = Omit & { uuid: string; type: 'component'; name: string; desc?: ElementBaseDesc & { - children: Array | DesignComponent>; + default: DesignComponentItem; + variants: DesignComponentItem[]; }; }; diff --git a/packages/design/src/types/index.ts b/packages/design/src/types/index.ts index d90f396..11a2f1e 100644 --- a/packages/design/src/types/index.ts +++ b/packages/design/src/types/index.ts @@ -1,2 +1,3 @@ export * from './data'; +export * from './context'; export * from './view'; diff --git a/packages/design/src/types/view.ts b/packages/design/src/types/view.ts index 0a4b83b..03e46aa 100644 --- a/packages/design/src/types/view.ts +++ b/packages/design/src/types/view.ts @@ -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[]; } diff --git a/packages/design/src/util/component.ts b/packages/design/src/util/component.ts index 4085a4a..a04b7ba 100644 --- a/packages/design/src/util/component.ts +++ b/packages/design/src/util/component.ts @@ -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; } diff --git a/packages/design/src/util/view-data.ts b/packages/design/src/util/view-data.ts new file mode 100644 index 0000000..0b5c870 --- /dev/null +++ b/packages/design/src/util/view-data.ts @@ -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; +} diff --git a/packages/design/src/util/view-tree.ts b/packages/design/src/util/view-tree.ts new file mode 100644 index 0000000..77b78b4 --- /dev/null +++ b/packages/design/src/util/view-tree.ts @@ -0,0 +1,70 @@ +import type { Element, ElementType } from '@idraw/types'; +import { ViewTreeNode, DesignComponent, DesignComponentItem } from '../types'; + +function parseElementToViewTreeNode(elem: Element): 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) => { + const childNode = parseElementToViewTreeNode(child); + if (childNode) { + treeNode?.children?.push(childNode); + } + }); + } + } + return treeNode; +} + +function parseComponentItemToViewTreeNode(comp: DesignComponentItem): ViewTreeNode { + const treeNode: Required = { + 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); + } + if (childNode) { + treeNode.children.push(childNode); + } + }); + } + return treeNode; +} + +export function parseComponentToViewTreeNode(comp: DesignComponent): ViewTreeNode { + const treeNode: Required = { + 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; +}