handleGutterMouseDown(ev, i)}
+ />
+ );
+ }
+ }
+
+ return elements;
+ };
+
+ return (
+
+ {renderChildren()}
+
+ );
+};
diff --git a/src/WBUtil.ts b/src/WBUtil.ts
index 21167b9..b7c2869 100644
--- a/src/WBUtil.ts
+++ b/src/WBUtil.ts
@@ -1,5 +1,5 @@
-import _ from 'lodash';
-import { IWBConfigData, IWBLayout, IWBLayoutComponent, IWBLayoutSplit, IWBSplitDirection } from './IWBLayout';
+import { sum } from 'lodash';
+import { IWBLayout, IWBComponent, IWBSplit, IWBSplitDir, IWBSplitConfig } from './IWBLayout';
let uid = 0;
@@ -28,63 +28,73 @@ export const WBUtil = {
}
},
- getAllLayouts(config: IWBConfigData) {
- const list: IWBLayout[] = [];
-
- this.walkLayout(config.layout, cur => {
- list.push(cur);
- });
-
- // side panel
- if (config.sidePanel) {
- for (const _sp of config.sidePanel.list) {
- this.walkLayout(_sp.layout, cur => {
- list.push(cur);
- });
- }
- }
-
- return list;
+ createSplit(
+ children: IWBLayout[],
+ config: IWBSplitConfig[] = children.map(c => ({ type: 'flex' })),
+ direction: IWBSplitDir = 'horizontal'
+ ): IWBSplit {
+ return { type: 'Split', key: this.randomID(), direction, children, config };
},
- createSplit(children: IWBLayout[], direction: IWBSplitDirection = 'horizontal', ratio = 0.5): IWBLayoutSplit {
- return { type: 'Split', key: this.randomID(), ratio, direction, children };
- },
-
- createComponent(component: string, query?: any): IWBLayoutComponent {
+ createComponent(component: string, query?: any): IWBComponent {
return { type: 'Component', key: this.randomID(), component, query };
},
- resetLayout(layout: IWBLayout, newData: IWBLayout) {
- // 原地修改 layout,保持引用
- Object.keys(layout).forEach(k => delete (layout as any)[k]);
- Object.assign(layout, newData);
- },
+ calcSplitSize(config: IWBSplit['config'], totalSize: number) {
+ const sizeList: number[] = Array(config.length).fill(0);
+
+ // 1. 填充 fixed size
+ for (let i = 0; i < config.length; i++) {
+ const c = config[i];
+ if (c.type === 'fixed') sizeList[i] = c.size;
+ }
- splitPanel(layout: IWBLayoutComponent, direction: IWBSplitDirection, lay2?: IWBLayout) {
- if (!lay2) lay2 = this.createComponent(layout.component, _.cloneDeep(layout.query));
- if (lay2.key === layout.key) throw new Error('duplicated key: ' + lay2.key);
+ // 2. 填充 flex size
+ const flexCount = sizeList.filter(s => s === 0).length;
+ const flexPerSize = (totalSize - sum(sizeList)) / flexCount;
+ for (let i = 0; i < config.length; i++) {
+ const c = config[i];
+ if (c.type === 'flex') sizeList[i] = flexPerSize;
+ }
- const newLayout = this.createSplit([_.cloneDeep(layout), lay2], direction);
- this.resetLayout(layout, newLayout);
+ return sizeList;
},
- closePanel(config: IWBConfigData, closeKey: string) {
- let closeParent: IWBLayout | undefined;
+ moveSplit(config: IWBSplit['config'], index: number, totalSize: number, movement: number) {
+ const c1 = config[index];
+ const c2 = config[index + 1];
- this.walkLayout(config.layout, (_cur, _parent) => {
- if (_cur.key === closeKey) {
- closeParent = _parent;
- return 'stop';
- }
- });
+ const minSize = 32;
+
+ const sizeList = this.calcSplitSize(config, totalSize);
+ const size1 = sizeList[index];
+ const size2 = sizeList[index + 1];
+
+ let replaceC1: IWBSplitConfig | undefined = undefined;
- if (!closeParent) throw new Error('missing closeParent');
- if (closeParent.type !== 'Split') throw new Error('closeParent.type error');
+ if (c1.type === 'fixed' && c2.type === 'fixed') {
+ const _m = movement > 0 ? Math.min(movement, size2 - minSize) : Math.max(movement, minSize - size1); // 防止超量
- const toKeepLayout = closeParent.children.find(c => c.key !== closeKey);
- if (!toKeepLayout) throw new Error('missing toKeepLayout');
+ c1.size = size1 + _m;
+ c2.size = size2 - _m;
+ }
+ // [fixed, flex]
+ else if (c1.type === 'fixed' && c2.type === 'flex') {
+ const _m = Math.max(movement, minSize - size1); // 防止超量
+ c1.size = size1 + _m;
+ }
+ // [flex, fixed]
+ else if (c1.type === 'flex' && c2.type === 'fixed') {
+ const _m = Math.min(movement, size2 - minSize); // 防止超量
+ c2.size = size2 - _m;
+ }
+ // [flex, flex]
+ else {
+ // 上一个变为 fixed
+ const _m = Math.max(movement, minSize - size1); // 防止超量
+ replaceC1 = { type: 'fixed', size: size1 + _m };
+ }
- this.resetLayout(closeParent, toKeepLayout);
+ if (replaceC1) config[index] = replaceC1;
},
};
diff --git a/src/Workbench.tsx b/src/Workbench.tsx
new file mode 100644
index 0000000..1e13c21
--- /dev/null
+++ b/src/Workbench.tsx
@@ -0,0 +1,77 @@
+import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { IWBLayout, IWBComponent } from './IWBLayout';
+import cx from 'classnames';
+import './style.less';
+import _ from 'lodash';
+import { WBLayout } from './WBLayout';
+
+export interface IWorkbenchProps {
+ className?: string;
+ style?: React.CSSProperties;
+
+ components: Record
;
+ layout: IWBLayout;
+ onChange?: (layout: IWBLayout) => any;
+ onEnterPanel?: (layout: IWBComponent) => any;
+}
+
+export const Workbench = ({ className, style, layout, onChange, components }: IWorkbenchProps) => {
+ const container = useRef(null);
+ const [bounding, setBounding] = useState<{ width: number; height: number }>();
+
+ const [stash, setStash] = useState(layout);
+
+ useEffect(() => {
+ setStash(layout);
+ }, [layout]);
+
+ const reloadBounding = () => {
+ if (!container.current) return;
+
+ const { width, height } = container.current.getBoundingClientRect();
+ setBounding({ width, height });
+ };
+
+ useLayoutEffect(() => {
+ if (!container.current) return;
+
+ reloadBounding();
+
+ // 监听 container 的 resize 事件
+ const ro = new ResizeObserver(() => reloadBounding());
+ ro.observe(container.current);
+
+ return () => {
+ ro.disconnect();
+ };
+ }, []);
+
+ const handleLayoutChange = (layout: IWBLayout) => {
+ setStash(layout);
+ onChange && onChange(layout);
+ };
+
+ const renderInner = () => {
+ if (!bounding) return null;
+
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+ {renderInner()}
+
+ );
+};
diff --git a/src/index.ts b/src/index.ts
index e03e874..ff9fa82 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,10 +1,7 @@
-export * from './FWContext';
-export * from './FlexibleComponent';
-export * from './FlexibleLayout';
-export * from './FlexibleSplit';
-export * from './FlexibleWorkbench';
+export * from './WBContext';
+export * from './WBComponent';
+export * from './WBLayout';
+export * from './WBSplit';
+export * from './Workbench';
export * from './IWBLayout';
export * from './WBUtil';
-export * from './useFWHeaderSlot';
-export * from './useFWPanelSize';
-export * from './useFWQuery';
diff --git a/src/style.less b/src/style.less
index 921f6ec..019046e 100644
--- a/src/style.less
+++ b/src/style.less
@@ -11,6 +11,11 @@
@component-header-height: 38px;
.FW {
+ &-wrapper {
+ width: 100%;
+ height: 100%;
+ }
+
position: relative;
width: 100%;
height: 100%;
@@ -21,12 +26,6 @@
position: relative;
height: 100%;
- > .ca {
- position: absolute;
- top: 0;
- left: 0;
- }
-
> .gutter {
position: absolute;
box-sizing: border-box;
@@ -45,22 +44,20 @@
transition: opacity 0.1s ease-in-out;
}
- &:hover {
+ &:hover,
+ &.active {
&::after {
opacity: 1;
}
}
}
- > .cb {
+ > .FW-component {
position: absolute;
- right: 0;
- bottom: 0;
}
&.vertical {
- > .ca,
- > .cb {
+ > .FW-component {
width: 100%;
}
> .gutter {
@@ -76,8 +73,7 @@
}
&.horizontal {
- > .ca,
- > .cb {
+ > .FW-component {
height: 100%;
}
> .gutter {
@@ -96,169 +92,7 @@
// 容器内部的内容
&-component {
height: 100%;
+ width: 100%;
box-sizing: border-box;
-
- > .header {
- display: flex;
- padding: 0 12px;
- height: @component-header-height;
- align-items: center;
- justify-content: space-between;
- border: 1px solid @colorBorderSecondary;
- box-sizing: border-box;
- user-select: none;
- color: @colorText;
- background: @colorBgContainer;
- border-top-left-radius: @component-border-radius;
- border-top-right-radius: @component-border-radius;
-
- > .title {
- display: flex;
- width: 1px;
- flex: 1;
- align-items: center;
- height: @component-header-height;
- box-sizing: border-box;
-
- > .main {
- height: @component-header-height;
- line-height: @component-header-height;
- font-size: 14px;
- font-weight: bolder;
- }
-
- > .internal-slot {
- margin-left: 12px;
- width: 1px;
- flex: 1;
- overflow: auto;
-
- &::-webkit-scrollbar {
- width: 0;
- height: 0;
- }
- }
- }
-
- > .extra {
- margin-left: 12px;
- }
-
- &.no-extra {
- > .title {
- > .main {
- flex: 1;
- }
-
- > .internal-slot {
- flex: unset;
- width: unset;
- }
- }
- }
- }
-
- > .body {
- position: relative;
- padding: 8px 12px;
- width: 100%;
- height: calc(100% - @component-header-height);
- background: @colorBgContainer;
- border-bottom-left-radius: @component-border-radius;
- border-bottom-right-radius: @component-border-radius;
- box-sizing: border-box;
-
- border: 1px solid @colorBorderSecondary;
- border-top: none;
-
- overflow: auto;
-
- &::-webkit-scrollbar {
- width: 0;
- height: 0;
- }
- }
- }
-
- // side panel
- &-SidePanelContainer {
- position: relative;
- width: 42px;
-
- &.expanded {
- &::after {
- position: absolute;
- content: '';
- width: calc(100% - 6px);
- height: 100%;
- left: 0;
- top: 0;
- border-top: 1px solid @colorBorder;
- border-right: 1px solid @colorBorder;
- border-bottom: 1px solid @colorBorder;
- pointer-events: none;
- border-radius: 4px;
- }
- }
-
- &-side {
- position: absolute;
- left: 0;
- top: 0;
- width: 36px;
- height: 100%;
- margin: 0;
- padding: 0;
-
- color: @colorText;
- list-style: none;
-
- > li {
- margin-bottom: 4px;
- width: 36px;
- height: 36px;
- cursor: pointer;
- border-radius: 4px;
- overflow: hidden;
-
- display: flex;
- align-items: center;
- justify-content: center;
-
- &:hover {
- background-color: @colorBgElevated;
- }
-
- &.active {
- background-color: @colorPrimary;
- }
- }
- }
-
- &-main {
- margin: 0 6px 0 42px;
- height: 100%;
-
- color: @colorText;
- background: @colorBgContainer;
- border-radius: 4px;
-
- overflow: auto;
-
- &::-webkit-scrollbar {
- width: 0;
- height: 0;
- }
- }
-
- &-resizer {
- position: absolute;
- right: 0;
- top: 0;
- width: 6px;
- height: 100%;
-
- cursor: col-resize;
- }
}
}
diff --git a/src/useFWHeaderSlot.ts b/src/useFWHeaderSlot.ts
deleted file mode 100644
index 1318805..0000000
--- a/src/useFWHeaderSlot.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useContext } from 'react';
-import ReactDOM from 'react-dom';
-import { FWContext } from './FWContext';
-
-export const useFWHeaderSlot = () => {
- const ctx = useContext(FWContext);
- const key = ctx.currentLayout.key;
-
- return (node: React.ReactNode) => {
- const slotEle = document.getElementById(`FW-internal-slot-${key}`);
- if (!slotEle) throw new Error('missing slot: ' + key);
-
- return ReactDOM.createPortal(node, slotEle);
- };
-};
diff --git a/src/useFWPanelSize.ts b/src/useFWPanelSize.ts
deleted file mode 100644
index b1aa7d4..0000000
--- a/src/useFWPanelSize.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useContext, useEffect, useLayoutEffect, useState } from 'react';
-import { useListen } from './useListen';
-import { FWContext } from './FWContext';
-
-export const useFWPanelSize = () => {
- const ctx = useContext(FWContext);
-
- const calcSize = () => {
- const bodyEle = document.getElementById(`FW-component-body-${ctx.currentLayout.key}`);
- if (!bodyEle) throw new Error('missing bodyEle: ' + ctx.currentLayout.key);
-
- const _bodySize = bodyEle.getBoundingClientRect();
- return { width: _bodySize.width, height: _bodySize.height };
- };
-
- const [size, setSize] = useState<{ width: number; height: number }>(calcSize());
-
- useListen(ctx.event, 'reflow', () => setSize(calcSize()));
- useLayoutEffect(() => setSize(calcSize()), []);
-
- return size;
-};
diff --git a/src/useFWQuery.ts b/src/useFWQuery.ts
deleted file mode 100644
index 4de95c2..0000000
--- a/src/useFWQuery.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { useContext } from 'react';
-import { FWContext } from './FWContext';
-import { useForceUpdate } from './useForceUpdate';
-
-export const useFWQuery = (updatable?: boolean) => {
- const ctx = useContext(FWContext);
- const fu = useForceUpdate();
-
- const layout = ctx.currentLayout;
-
- const getQuery = (): T | undefined => {
- return layout.type === 'Component' ? layout.query : undefined;
- };
- const query = getQuery();
-
- const setQuery = (q: T | undefined) => {
- if (layout.type === 'Component') {
- layout.query = q;
- ctx.event.emit('afterConfigChange', { source: layout, skipRefresh: true });
-
- if (updatable) fu.update();
- }
- };
-
- const mergeQuery = (q: Partial | undefined) => {
- setQuery({ ...getQuery(), ...q } as any);
- };
-
- return { query, setQuery, getQuery, mergeQuery };
-};
diff --git a/src/useHandler.ts b/src/useHandler.ts
deleted file mode 100644
index 63ee0ae..0000000
--- a/src/useHandler.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useRef } from 'react';
-
-export const useHandler = any>(fn: T) => {
- const cc = useRef(fn);
- cc.current = fn; // 始终指向最新闭包函数
-
- const handlerRef = useRef((...args: any[]) => cc.current(...args));
- return handlerRef.current as T;
-};