diff --git a/packages/designer/src/builtin-simulator/host.less b/packages/designer/src/builtin-simulator/host.less index 120c9bfb3..ea009f455 100644 --- a/packages/designer/src/builtin-simulator/host.less +++ b/packages/designer/src/builtin-simulator/host.less @@ -46,12 +46,12 @@ } &-device-default { - top: 15px; - right: 15px; - bottom: 15px; - left: 15px; + top: 16px; + right: 16px; + bottom: 16px; + left: 16px; width: auto; - box-shadow: 0 2px 10px 0 rgba(31,56,88,.15); + box-shadow: 0 1px 4px 0 rgba(31, 50, 88, 0.125); } &-content { diff --git a/packages/designer/src/designer/designer-view.tsx b/packages/designer/src/designer/designer-view.tsx index 3fff56f60..6aef22c50 100644 --- a/packages/designer/src/designer/designer-view.tsx +++ b/packages/designer/src/designer/designer-view.tsx @@ -8,7 +8,7 @@ import './designer.less'; import clipboard from './clipboard'; export class DesignerView extends Component { readonly designer: Designer; diff --git a/packages/editor-core/src/areaManager.ts b/packages/editor-core/src/areaManager.ts index f6ce5e782..6f162c5cc 100644 --- a/packages/editor-core/src/areaManager.ts +++ b/packages/editor-core/src/areaManager.ts @@ -7,18 +7,28 @@ export default class AreaManager { private config: PluginConfig[]; - private editor: Editor; - - private area: string; - - constructor(editor: Editor, area: string) { - this.editor = editor; - this.area = area; - this.config = (editor && editor.config && editor.config.plugins && editor.config.plugins[this.area]) || []; + constructor(private editor: Editor, private name: string) { + this.config = (editor && editor.config && editor.config.plugins && editor.config.plugins[name]) || []; this.pluginStatus = clone(editor.pluginStatus); } - public isPluginStatusUpdate(pluginType?: string, notUpdateStatus?: boolean): boolean { + setVisible(flag: boolean) { + + } + + isEnable() { + + } + + isVisible() { + + } + + isEmpty() { + + } + + isPluginStatusUpdate(pluginType?: string, notUpdateStatus?: boolean): boolean { const { pluginStatus } = this.editor; const list = pluginType ? this.config.filter((item): boolean => item.type === pluginType) : this.config; @@ -31,33 +41,33 @@ export default class AreaManager { return isUpdate; } - public getVisiblePluginList(pluginType?: string): PluginConfig[] { + getVisiblePluginList(pluginType?: string): PluginConfig[] { const res = this.config.filter((item): boolean => { return !!(!this.pluginStatus[item.pluginKey] || this.pluginStatus[item.pluginKey].visible); }); return pluginType ? res.filter((item): boolean => item.type === pluginType) : res; } - public getPlugin(pluginKey: string): HOCPlugin | void { + getPlugin(pluginKey: string): HOCPlugin | void { if (pluginKey) { return this.editor && this.editor.plugins && this.editor.plugins[pluginKey]; } } - public getPluginConfig(pluginKey?: string): PluginConfig[] | PluginConfig | undefined { + getPluginConfig(pluginKey?: string): PluginConfig[] | PluginConfig | undefined { if (pluginKey) { return this.config.find(item => item.pluginKey === pluginKey); } return this.config; } - public getPluginClass(pluginKey: string): PluginClass | void { + getPluginClass(pluginKey: string): PluginClass | void { if (pluginKey) { return this.editor && this.editor.components && this.editor.components[pluginKey]; } } - public getPluginStatus(pluginKey: string): PluginStatus | void { + getPluginStatus(pluginKey: string): PluginStatus | void { if (pluginKey) { return this.editor && this.editor.pluginStatus && this.editor.pluginStatus[pluginKey]; } diff --git a/packages/editor-core/src/editor.ts b/packages/editor-core/src/editor.ts index 6f9a1a01f..25730a95b 100644 --- a/packages/editor-core/src/editor.ts +++ b/packages/editor-core/src/editor.ts @@ -91,7 +91,7 @@ export default class Editor extends EventEmitter { private hooksFuncs: HooksFuncs; - constructor(config: EditorConfig, components: PluginClassSet, utils?: Utils) { + constructor(config: EditorConfig = {}, components: PluginClassSet = {}, utils?: Utils) { super(); this.config = config; this.components = {}; diff --git a/packages/plugin-designer/src/index.tsx b/packages/plugin-designer/src/index.tsx index 222ef7a91..c1513ff08 100644 --- a/packages/plugin-designer/src/index.tsx +++ b/packages/plugin-designer/src/index.tsx @@ -1,152 +1,12 @@ import React, { PureComponent } from 'react'; -import { Editor, PluginConfig } from '@ali/lowcode-editor-core'; +import { Editor } from '@ali/lowcode-editor-core'; import { DesignerView, Designer } from '@ali/lowcode-designer'; import './index.scss'; export interface PluginProps { editor: Editor; - config: PluginConfig; } -const SCHEMA = { - version: '1.0', - componentsMap: [], - componentsTree: [ - { - componentName: 'Page', - fileName: 'test', - dataSource: { - list: [], - }, - state: { - text: 'outter', - }, - props: { - ref: 'outterView', - autoLoading: true, - style: { - padding: 20, - }, - }, - children: [ - { - componentName: 'Form', - props: { - labelCol: 3, - style: {}, - ref: 'testForm', - }, - children: [ - { - componentName: 'Form.Item', - props: { - label: '姓名:', - name: 'name', - initValue: '李雷', - }, - children: [ - { - componentName: 'Input', - props: { - placeholder: '请输入', - size: 'medium', - style: { - width: 320, - }, - }, - }, - ], - }, - { - componentName: 'Form.Item', - props: { - label: '年龄:', - name: 'age', - initValue: '22', - }, - children: [ - { - componentName: 'NumberPicker', - props: { - size: 'medium', - type: 'normal', - }, - }, - ], - }, - { - componentName: 'Form.Item', - props: { - label: '职业:', - name: 'profession', - }, - children: [ - { - componentName: 'Select', - props: { - dataSource: [ - { - label: '教师', - value: 't', - }, - { - label: '医生', - value: 'd', - }, - { - label: '歌手', - value: 's', - }, - ], - }, - }, - ], - }, - { - componentName: 'Div', - props: { - style: { - textAlign: 'center', - }, - }, - children: [ - { - componentName: 'Button.Group', - props: {}, - children: [ - { - componentName: 'Button', - props: { - type: 'primary', - style: { - margin: '0 5px 0 5px', - }, - htmlType: 'submit', - }, - children: '提交', - }, - { - componentName: 'Button', - props: { - type: 'normal', - style: { - margin: '0 5px 0 5px', - }, - htmlType: 'reset', - }, - children: '重置', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], -}; - interface DesignerPluginState { componentMetadatas?: any[] | null; library?: any[] | null; diff --git a/packages/plugin-outline-pane/src/main.ts b/packages/plugin-outline-pane/src/main.ts index debd7a734..8348691d7 100644 --- a/packages/plugin-outline-pane/src/main.ts +++ b/packages/plugin-outline-pane/src/main.ts @@ -168,7 +168,6 @@ export class OutlineMain implements ISensor, IScrollBoard, IScrollable { } } }); - // editor.once('outlinePane.visible', setup); } } diff --git a/packages/vision-polyfill/package.json b/packages/vision-polyfill/package.json index 01e5dded8..5cd16c69e 100644 --- a/packages/vision-polyfill/package.json +++ b/packages/vision-polyfill/package.json @@ -28,6 +28,7 @@ "@ali/lowcode-plugin-zh-en": "^0.8.6", "@ali/lowcode-setters": "^0.8.6", "@alifd/next": "^1.19.12", + "@ali/ve-less-variables": "2.0.3", "@alife/theme-lowcode-dark": "^0.1.0", "@alife/theme-lowcode-light": "^0.1.0", "react": "^16.8.1", diff --git a/packages/vision-polyfill/src/demo.ts b/packages/vision-polyfill/src/demo.ts index b980feffd..a93a5f930 100644 --- a/packages/vision-polyfill/src/demo.ts +++ b/packages/vision-polyfill/src/demo.ts @@ -1,5 +1,5 @@ -import { init } from "./vision"; -import editor from './editor'; +import { init } from './vision'; // VisualEngine +import { editor } from './editor'; init(); diff --git a/packages/vision-polyfill/src/editor.ts b/packages/vision-polyfill/src/editor.ts index 60989133c..46dccc510 100644 --- a/packages/vision-polyfill/src/editor.ts +++ b/packages/vision-polyfill/src/editor.ts @@ -1,68 +1,44 @@ import Editor from '@ali/lowcode-editor-core'; -import outlinePane from '@ali/lowcode-plugin-outline-pane'; -import settingsPane from '@ali/lowcode-plugin-settings-pane'; -import designer from '@ali/lowcode-plugin-designer'; +import OutlinePane from '@ali/lowcode-plugin-outline-pane'; +import SettingsPane from '@ali/lowcode-plugin-settings-pane'; +import Designer from '@ali/lowcode-plugin-designer'; import { registerSetters } from '@ali/lowcode-setters'; +import { Skeleton } from './skeleton/skeleton'; registerSetters(); -export default new Editor( - { - plugins: { - topArea: [], - leftArea: [ - { - pluginKey: 'outlinePane', - type: 'PanelIcon', - props: { - align: 'top', - icon: 'shuxingkongjian', - title: '大纲树', - }, - config: { - package: '@ali/lowcode-plugin-outline-pane', - version: '^0.8.0', - }, - pluginProps: {}, - }, - ], - rightArea: [ - { - pluginKey: 'settingsPane', - type: 'Panel', - props: {}, - config: { - package: '@ali/lowcode-plugin-settings-pane', - version: '^0.8.0', - }, - pluginProps: {}, - }, - ], - centerArea: [ - { - pluginKey: 'designer', - type: '', - props: {}, - config: { - package: '@ali/lowcode-plugin-designer', - version: '^0.8.0', - }, - }, - ], - }, - }, - { - outlinePane, - settingsPane, - designer, - }, -); +export const editor = new Editor(); +export const skeleton = new Skeleton(editor); + +skeleton.mainArea.add({ + name: 'designer', + type: 'Widget', + content: Designer, +}); +skeleton.rightArea.add({ + name: 'settingsPane', + type: 'Panel', + content: SettingsPane, +}); +skeleton.leftArea.add({ + name: 'outlinePane', + type: 'PanelDock', + props: { + align: 'top', + icon: 'shuxingkongjian', + description: '大纲树', + }, + content: OutlinePane, + panelProps: { + area: 'leftFloatArea' + } +}); // editor-core // 1. di 实现 -// 2. skeleton 区域管理 -// 3. general bus: pub/sub +// 2. general bus: pub/sub // editor-skeleton/workbench 视图实现 +// 1. skeleton 区域划分 panes // provide fixed left pane // provide float left pane diff --git a/packages/vision-polyfill/src/flags.ts b/packages/vision-polyfill/src/flags.ts new file mode 100644 index 000000000..b9921ece6 --- /dev/null +++ b/packages/vision-polyfill/src/flags.ts @@ -0,0 +1,148 @@ +import domReady = require('domready'); +import * as EventEmitter from 'events'; + +const Shells = ['iphone6']; + +export class Flags { + + public emitter: EventEmitter; + public flags: string[]; + public ready: boolean; + public lastFlags: string[]; + public lastShell: string; + + private lastSimulatorDevice: string; + + constructor() { + this.emitter = new EventEmitter(); + this.flags = ['design-mode']; + + domReady(() => { + this.ready = true; + this.applyFlags(); + }); + } + + public setDragMode(flag: boolean) { + if (flag) { + this.add('drag-mode'); + } else { + this.remove('drag-mode'); + } + } + + public setPreviewMode(flag: boolean) { + if (flag) { + this.add('preview-mode'); + this.remove('design-mode'); + } else { + this.add('design-mode'); + this.remove('preview-mode'); + } + } + + public setWithShell(shell: string) { + if (shell === this.lastShell) { + return; + } + if (this.lastShell) { + this.remove(`with-${this.lastShell}shell`); + } + if (shell) { + if (Shells.indexOf(shell) < 0) { + shell = Shells[0]; + } + this.add(`with-${shell}shell`); + this.lastShell = shell; + } + } + + public setSimulator(device: string) { + if (this.lastSimulatorDevice) { + this.remove(`simulator-${this.lastSimulatorDevice}`); + } + if (device !== '' && device !== 'pc') { + this.add(`simulator-${device}`); + } + this.lastSimulatorDevice = device; + } + + public setHideSlate(flag: boolean) { + if (this.has('slate-fixed')) { + return; + } + if (flag) { + this.add('hide-slate'); + } else { + this.remove('hide-slate'); + } + } + + public setSlateFixedMode(flag: boolean) { + if (flag) { + this.remove('hide-slate'); + this.add('slate-fixed'); + } else { + this.remove('slate-fixed'); + } + } + + public setSlateFullMode(flag: boolean) { + if (flag) { + this.add('slate-full-screen'); + } else { + this.remove('slate-full-screen'); + } + } + + public getFlags() { + return this.flags; + } + + public applyFlags(modifiedFlag?: string) { + if (!this.ready) { + return; + } + + const doe = document.documentElement; + if (this.lastFlags) { + this.lastFlags.filter((flag: string) => this.flags.indexOf(flag) < 0).forEach((flag) => { + doe.classList.remove(`engine-${flag}`); + }); + } + this.flags.forEach((flag) => { + doe.classList.add(`engine-${flag}`); + }); + + this.lastFlags = this.flags.slice(0); + this.emitter.emit('flagschange', this.flags, modifiedFlag); + } + + public has(flag: string) { + return this.flags.indexOf(flag) > -1; + } + + public add(flag: string) { + if (!this.has(flag)) { + this.flags.push(flag); + this.applyFlags(flag); + } + } + + public remove(flag: string) { + const i = this.flags.indexOf(flag); + if (i > -1) { + this.flags.splice(i, 1); + this.applyFlags(flag); + } + } + + public onFlagsChange(func: () => any) { + this.emitter.on('flagschange', func); + return () => { + this.emitter.removeListener('flagschange', func); + }; + } +} + +export default new Flags(); diff --git a/packages/vision-polyfill/src/panes.ts b/packages/vision-polyfill/src/panes.ts new file mode 100644 index 000000000..6ccadccf8 --- /dev/null +++ b/packages/vision-polyfill/src/panes.ts @@ -0,0 +1,206 @@ +import { skeleton } from './editor'; +import { ReactElement } from 'react'; +import { IWidgetBaseConfig } from './skeleton/types'; + +export interface IContentItemConfig { + title: string; + content: JSX.Element; + tip?: { + content: string; + url?: string; + }; +} + +export interface OldPaneConfig { + // 'dock' | 'action' | 'tab' | 'widget' | 'stage' + type?: string; // where + + id?: string; + name: string; + title?: string; + content?: any; + + place?: string; // align: left|right|top|center|bottom + description?: string; // tip? + tip?: + | string + | { + // as help tip + url?: string; + content?: string | JSX.Element; + }; // help + + init?: () => any; + destroy?: () => any; + props?: any; + + contents?: IContentItemConfig[]; + hideTitleBar?: boolean; + width?: number; + maxWidth?: number; + height?: number; + maxHeight?: number; + position?: string | string[]; // todo + menu?: JSX.Element; // as title + index?: number; // todo + isAction?: boolean; // as normal dock + fullScreen?: boolean; // todo +} + +function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: string } { + const { type, id, name, title, content, place, description, init, destroy, props, index } = config; + + const newConfig: any = { + id, + name, + content, + props: { + title, + description, + align: place, + onInit: init, + onDestroy: destroy, + }, + contentProps: props, + index, + }; + if (type === 'dock') { + newConfig.type = 'PanelDock'; + newConfig.area = 'left'; + const { contents, hideTitleBar, tip, width, maxWidth, height, maxHeight, position, menu, isAction } = config; + if (menu) { + newConfig.props.title = menu; + } + if (!isAction) { + newConfig.panelProps = { + hideTitleBar, + help: tip, + width, + maxWidth, + height, + maxHeight, + }; + + if (contents && Array.isArray(contents)) { + newConfig.content = contents.map(({ title, content, tip }) => { + return { + type: "Panel", + content, + props: { + title, + help: tip, + } + } + }); + } + } + } else if (type === 'action') { + newConfig.area = 'top'; + newConfig.type = 'Dock'; + } else if (type === 'tab') { + newConfig.area = 'right'; + newConfig.type = 'Panel'; + } else if (type === 'stage') { + newConfig.area = 'stages'; + newConfig.type = 'Widget'; + } else { + newConfig.area = 'main'; + newConfig.type = 'Widget'; + } + + return newConfig; +} + +function add(config: (() => OldPaneConfig) | OldPaneConfig, extraConfig?: any) { + if (typeof config === 'function') { + config = config.call(null); + } + if (!config || !config.type) { + return null; + } + if (extraConfig) { + config = { ...config, ...extraConfig }; + } + + skeleton.add(upgradeConfig(config)); +} + +const actionPane = Object.assign(skeleton.topArea, { + /** + * compatible *VE.actionPane.getActions* + */ + getActions(): any { + return skeleton.topArea.container.items; + }, + /** + * compatible *VE.actionPane.activeDock* + */ + setActions() { + // empty + }, +}); +const dockPane = Object.assign(skeleton.leftArea, { + /** + * compatible *VE.dockPane.activeDock* + */ + activeDock(item: any) { + const name = item.name || item; + skeleton.getPanel(name)?.active(); + }, + + /** + * compatible *VE.dockPane.onDockShow* + */ + onDockShow() {}, + /** + * compatible *VE.dockPane.onDockHide* + */ + onDockHide() {}, + /** + * compatible *VE.dockPane.setFixed* + */ + setFixed(flag: boolean) { + // todo: + }, +}); +const tabPane = Object.assign(skeleton.rightArea, { + setFloat(flag: boolean) { + // todo: + }, +}); +const toolbar = Object.assign(skeleton.toolbar, { + setContents(contents: ReactElement) { + // todo: + }, +}); +const widgets = skeleton.mainArea; + +const stages = Object.assign(skeleton.stages, { + getStage(name: string) { + skeleton.stages.container.get(name); + }, + + createStage(config: any) { + config = upgradeConfig(config); + if (config.id) { + config.name = config.id; + } + + const stage = skeleton.stages.add(config); + return stage.getName(); + }, +}); + +export default { + ActionPane: actionPane, // topArea + actionPane, // + DockPane: dockPane, // leftArea + dockPane, + TabPane: tabPane, // rightArea + tabPane, + add, + toolbar, // toolbar + Stages: stages, + Widgets: widgets, // centerArea + widgets, +}; diff --git a/packages/vision-polyfill/src/skeleton/area.ts b/packages/vision-polyfill/src/skeleton/area.ts new file mode 100644 index 000000000..bf68b9fa8 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/area.ts @@ -0,0 +1,48 @@ +import { obx, computed } from '@ali/lowcode-globals'; +import WidgetContainer from './widget-container'; +import { Skeleton } from './skeleton'; +import { IWidget } from './widget'; +import { IWidgetBaseConfig } from './types'; + +export default class Area { + @obx private _visible: boolean = true; + + @computed get visible() { + if (this.exclusive) { + return this.container.current != null; + } + return this._visible; + } + + readonly container: WidgetContainer; + constructor(readonly skeleton: Skeleton, readonly name: string, handle: (item: T | C) => T, private exclusive?: boolean, defaultSetCurrent: boolean = false) { + this.container = skeleton.createContainer(name, handle, exclusive, () => this.visible, defaultSetCurrent); + } + + @computed isEmpty(): boolean { + return this.container.items.length < 1; + } + + add(config: T | C): T { + return this.container.add(config); + } + + private lastCurrent: T | null = null; + setVisible(flag: boolean) { + if (this.exclusive) { + const current = this.container.current; + if (flag && !current) { + this.container.active(this.lastCurrent || this.container.getAt(0)) + } else if (current) { + this.lastCurrent = this.container.current; + this.container.unactive(); + } + return; + } + this._visible = flag; + } + + hide() { + this.setVisible(false); + } +} diff --git a/packages/vision-polyfill/src/skeleton/bottom-area.tsx b/packages/vision-polyfill/src/skeleton/bottom-area.tsx new file mode 100644 index 000000000..2aad04150 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/bottom-area.tsx @@ -0,0 +1,38 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/recore'; +import Area from './area'; +import Panel from './panel'; +import { PanelWrapper } from './widget-views'; + +@observer +export default class BottomArea extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + if (area.isEmpty()) { + return null; + } + return ( +
+ +
+ ); + } +} + +@observer +class Contents extends Component<{ area: Area }> { + render() { + const { area } = this.props; + return ( + + {area.container.items.map((item) => )} + + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/dialog-dock.ts b/packages/vision-polyfill/src/skeleton/dialog-dock.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/vision-polyfill/src/skeleton/dock.ts b/packages/vision-polyfill/src/skeleton/dock.ts new file mode 100644 index 000000000..20952c766 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/dock.ts @@ -0,0 +1,78 @@ +import { ReactNode, createElement } from 'react'; +import { uniqueId, createContent, obx } from '@ali/lowcode-globals'; +import { DockConfig } from "./types"; +import { Skeleton } from './skeleton'; +import { DockView } from './widget-views'; +import { IWidget } from './widget'; + +/** + * 带图标(主要)/标题(次要)的扩展 + */ +export default class Dock implements IWidget { + readonly isWidget = true; + readonly id = uniqueId('dock'); + readonly name: string; + readonly align?: string; + + @obx.ref private _visible: boolean = true; + get visible(): boolean { + return this._visible; + } + + private inited: boolean = false; + private _content: ReactNode; + get content() { + if (this.inited) { + return this._content; + } + this.inited = true; + const { props, content, contentProps } = this.config; + + if (content) { + this._content = createContent(content, { + ...contentProps, + editor: this.skeleton.editor, + key: this.id, + }); + } else { + this._content = createElement(DockView, { + ...props, + key: this.id, + }); + } + + return this._content; + } + constructor(readonly skeleton: Skeleton, private config: DockConfig) { + const { props = {}, name } = config; + this.name = name; + this.align = props.align; + } + + setVisible(flag: boolean) { + if (flag === this._visible) { + return; + } + if (flag) { + this._visible = true; + } else if (this.inited) { + this._visible = false; + } + } + + getContent() { + return this.content; + } + + getName() { + return this.name; + } + + hide() { + this.setVisible(false); + } + + show() { + this.setVisible(true); + } +} diff --git a/packages/vision-polyfill/src/skeleton/left-area.tsx b/packages/vision-polyfill/src/skeleton/left-area.tsx new file mode 100644 index 000000000..87df6170c --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/left-area.tsx @@ -0,0 +1,41 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-globals'; +import Area from './area'; + +@observer +export default class LeftArea extends Component<{ area: Area }> { + render() { + const { area } = this.props; + return ( +
+ +
+ ); + } +} + + +@observer +class Contents extends Component<{ area: Area }> { + render() { + const { area } = this.props; + const top: any[] = []; + const bottom: any[] = []; + area.container.items.forEach(item => { + if (item.align === 'bottom') { + bottom.push(item.content); + } else { + top.push(item.content); + } + }); + return ( + +
{top}
+
{bottom}
+
+ ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/left-fixed-pane.tsx b/packages/vision-polyfill/src/skeleton/left-fixed-pane.tsx new file mode 100644 index 000000000..cf2579f8b --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/left-fixed-pane.tsx @@ -0,0 +1,50 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-globals'; +import { Button } from '@alifd/next'; +import Area from './area'; +import { PanelConfig } from './types'; +import Panel from './panel'; +import { PanelWrapper } from './widget-views'; + +@observer +export default class LeftFixedPane extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( +
+
+ ); + } +} + +@observer +class Contents extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( + + {area.container.items.map((panel) => ( + + ))} + + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/left-float-pane.tsx b/packages/vision-polyfill/src/skeleton/left-float-pane.tsx new file mode 100644 index 000000000..4b551b143 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/left-float-pane.tsx @@ -0,0 +1,51 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-globals'; +import { Button } from '@alifd/next'; +import Area from './area'; +import Panel from './panel'; +import { PanelWrapper } from './widget-views'; + +@observer +export default class LeftFloatPane extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + // TODO: add focusingManager + // TODO: dragstart close + return ( +
+
+ ); + } +} + +@observer +class Contents extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( + + {area.container.items.map((panel) => ( + + ))} + + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/main-area.tsx b/packages/vision-polyfill/src/skeleton/main-area.tsx new file mode 100644 index 000000000..58eee2842 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/main-area.tsx @@ -0,0 +1,28 @@ +import { Component } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/recore'; +import Area from './area'; +import Panel, { isPanel } from './panel'; +import { PanelWrapper } from './widget-views'; +import Widget from './widget'; + +@observer +export default class MainArea extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( +
+ {area.container.items.map((item) => { + // todo? + if (isPanel(item)) { + return ; + } + return item.content; + })} +
+ ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/panel-dock.ts b/packages/vision-polyfill/src/skeleton/panel-dock.ts new file mode 100644 index 000000000..5877c864f --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/panel-dock.ts @@ -0,0 +1,79 @@ +import { uniqueId, obx, computed } from '@ali/lowcode-globals'; +import { createElement, ReactNode } from 'react'; +import { Skeleton } from './skeleton'; +import { PanelDockConfig } from './types'; +import Panel from './panel'; +import { PanelDockView } from './widget-views'; +import { IWidget } from './widget'; + +export default class PanelDock implements IWidget { + readonly isWidget = true; + readonly id: string; + readonly name: string; + readonly align?: string; + + private inited: boolean = false; + private _content: ReactNode; + get content() { + if (this.inited) { + return this._content; + } + this.inited = true; + const { props } = this.config; + + this._content = createElement(PanelDockView, { + ...props, + key: this.id, + dock: this, + }); + + return this._content; + } + + @computed get actived(): boolean { + return this.panel?.visible || false; + } + + readonly panelName: string; + private _panel?: Panel; + @computed get panel() { + return this._panel || this.skeleton.getPanel(this.panelName); + } + + constructor(readonly skeleton: Skeleton, private config: PanelDockConfig) { + const { content, contentProps, panelProps, name } = config; + this.name = name; + this.id = uniqueId(`dock:${name}$`); + this.panelName = config.panelName || name; + if (content) { + this._panel = this.skeleton.add({ + type: "Panel", + name: this.panelName, + props: panelProps || {}, + contentProps, + content, + area: panelProps?.area || 'leftFloatArea' + }) as Panel; + } + } + + toggle() { + this.panel?.toggle(); + } + + getName() { + return this.name; + } + + getContent() { + return this.content; + } + + hide() { + this.panel?.setActive(false); + } + + show() { + this.panel?.setActive(true); + } +} diff --git a/packages/vision-polyfill/src/skeleton/panel.ts b/packages/vision-polyfill/src/skeleton/panel.ts new file mode 100644 index 000000000..bf34b2e45 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/panel.ts @@ -0,0 +1,150 @@ +import {createElement, ReactNode } from 'react'; +import { obx, uniqueId, createContent, TitleContent } from '@ali/lowcode-globals'; +import WidgetContainer from './widget-container'; +import { PanelConfig, HelpTipConfig } from './types'; +import { PanelView, TabsPanelView } from './widget-views'; +import { Skeleton } from './skeleton'; +import { composeTitle } from './utils'; +import { IWidget } from './widget'; + +export default class Panel implements IWidget { + readonly isWidget = true; + readonly name: string; + readonly id: string; + @obx.ref inited: boolean = false; + @obx.ref private _actived: boolean = false; + get actived(): boolean { + return this._actived; + } + get visible(): boolean { + if (this.parent?.visible) { + return this._actived; + } + return false; + } + setActive(flag: boolean) { + if (flag === this._actived) { + // TODO: 如果移动到另外一个 container,会有问题 + return; + } + if (flag) { + if (!this.inited) { + this.initBody(); + } + this._actived = true; + this.parent?.active(this); + } else if (this.inited) { + this._actived = false; + this.parent?.unactive(this); + } + } + + toggle() { + this.setActive(!this._actived); + } + + readonly isPanel = true; + + private _body?: ReactNode; + get body() { + this.initBody(); + return this._body; + } + + get content() { + return this.plain ? this.body : createElement(PanelView, { panel: this }); + } + + readonly title: TitleContent; + readonly help?: HelpTipConfig; + private plain: boolean = false; + + private container?: WidgetContainer; + private parent?: WidgetContainer; + + constructor(readonly skeleton: Skeleton, private config: PanelConfig) { + const { name, content, props = {} } = config; + const { hideTitleBar, title, icon, description, help, shortcut } = props; + this.name = name; + this.id = uniqueId(`pane:${name}$`); + this.title = composeTitle(title || name, icon, description); + this.plain = hideTitleBar || !title; + this.help = help; + if (Array.isArray(content)) { + this.container = this.skeleton.createContainer(name, (item) => { + if (isPanel(item)) { + return item; + } + return this.skeleton.createPanel(item); + }, true, () => this.visible, true); + content.forEach(item => this.add(item)); + } + // todo: process shortcut + } + + private initBody() { + if (this.inited) { + return; + } + this.inited = true; + if (this.container) { + this._body = createElement(TabsPanelView, { + container: this.container, + key: this.id, + }); + } else { + const { content, contentProps } = this.config; + this._body = createContent(content, { + ...contentProps, + editor: this.skeleton.editor, + panel: this, + key: this.id, + }); + } + } + setParent(parent: WidgetContainer) { + if (parent === this.parent) { + return; + } + if (this.parent) { + this.parent.remove(this); + } + this.parent = parent; + } + + add(item: Panel | PanelConfig) { + return this.container?.add(item); + } + + getPane(name: string): Panel | null { + return this.container?.get(name) || null; + } + + remove(item: Panel | string) { + return this.container?.remove(item); + } + + active(item?: Panel | string | null) { + this.container?.active(item); + } + + getName() { + return this.name; + } + + getContent() { + return this.content; + } + + hide() { + this.setActive(false); + } + + show() { + this.setActive(true); + } +} + +export function isPanel(obj: any): obj is Panel { + return obj && obj.isPanel; +} diff --git a/packages/vision-polyfill/src/skeleton/right-area.tsx b/packages/vision-polyfill/src/skeleton/right-area.tsx new file mode 100644 index 000000000..b756b2868 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/right-area.tsx @@ -0,0 +1,36 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/recore'; +import Area from './area'; +import Panel from './panel'; +import { PanelWrapper } from './widget-views'; + +@observer +export default class RightArea extends Component<{ area: Area }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( +
+ +
+ ); + } +} + + +@observer +class Contents extends Component<{ area: Area }> { + render() { + const { area } = this.props; + return ( + + {area.container.items.map((item) => )} + + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/skeleton.ts b/packages/vision-polyfill/src/skeleton/skeleton.ts new file mode 100644 index 000000000..e0382b66e --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/skeleton.ts @@ -0,0 +1,194 @@ +import { + DockConfig, + PanelConfig, + WidgetConfig, + IWidgetBaseConfig, + PanelDockConfig, + DialogDockConfig, + isDockConfig, + isPanelDockConfig, + isPanelConfig, +} from './types'; +import Editor from '@ali/lowcode-editor-core'; +import Panel, { isPanel } from './panel'; +import WidgetContainer from './widget-container'; +import Area from './area'; +import Widget, { isWidget, IWidget } from './widget'; +import PanelDock from './panel-dock'; +import Dock from './dock'; +import { Stage, StageConfig } from './stage'; + +export class Skeleton { + private panels = new Map(); + private containers = new Map>(); + readonly leftArea: Area; + readonly topArea: Area; + readonly toolbar: Area; + readonly leftFixedArea: Area; + readonly leftFloatArea: Area; + readonly rightArea: Area; + readonly mainArea: Area; + readonly bottomArea: Area; + readonly stages: Area; + constructor(readonly editor: Editor) { + this.leftArea = new Area( + this, + 'leftArea', + (config) => { + if (isWidget(config)) { + return config; + } + return this.createWidget(config); + }, + false, + ); + this.topArea = new Area( + this, + 'topArea', + (config) => { + if (isWidget(config)) { + return config; + } + return this.createWidget(config); + }, + false, + ); + this.toolbar = new Area( + this, + 'toolbar', + (config) => { + if (isWidget(config)) { + return config; + } + return this.createWidget(config); + }, + false, + ); + this.leftFixedArea = new Area( + this, + 'leftFixedArea', + (config) => { + if (isPanel(config)) { + return config; + } + return this.createPanel(config); + }, + true, + ); + this.leftFloatArea = new Area( + this, + 'leftFloatArea', + (config) => { + if (isPanel(config)) { + return config; + } + return this.createPanel(config); + }, + true, + ); + this.rightArea = new Area( + this, + 'rightArea', + (config) => { + if (isPanel(config)) { + return config; + } + return this.createPanel(config); + }, + true, + true, + ); + this.mainArea = new Area( + this, + 'mainArea', + (config) => { + if (isWidget(config)) { + return config as Widget; + } + return this.createWidget(config) as Widget; + }, + true, + true, + ); + this.bottomArea = new Area( + this, + 'bottomArea', + (config) => { + if (isPanel(config)) { + return config; + } + return this.createPanel(config); + }, + true, + ); + this.stages = new Area(this, 'stages', (config) => { + if (isWidget(config)) { + return config; + } + return new Stage(this, config); + }); + } + + createWidget(config: IWidgetBaseConfig | IWidget) { + if (isWidget(config)) { + return config; + } + if (isDockConfig(config)) { + if (isPanelDockConfig(config)) { + return new PanelDock(this, config); + } + + return new Dock(this, config); + } + if (isPanelConfig(config)) { + return this.createPanel(config); + } + return new Widget(this, config as WidgetConfig); + } + + createPanel(config: PanelConfig) { + const panel = new Panel(this, config); + this.panels.set(panel.name, panel); + return panel; + } + + getPanel(name: string): Panel | undefined { + return this.panels.get(name); + } + + createContainer( + name: string, + handle: (item: any) => any, + exclusive: boolean = false, + checkVisible: () => boolean = () => true, + defaultSetCurrent: boolean = false, + ) { + const container = new WidgetContainer(name, handle, exclusive, checkVisible, defaultSetCurrent); + this.containers.set(name, container); + return container; + } + + add(config: IWidgetBaseConfig & { area: string }) { + const { area } = config; + switch (area) { + case 'leftArea': case 'left': + return this.leftArea.add(config as any); + case 'rightArea': case 'right': + return this.rightArea.add(config as any); + case 'topArea': case 'top': + return this.topArea.add(config as any); + case 'toolbar': + return this.toolbar.add(config as any); + case 'mainArea': case 'main': case 'center': case 'centerArea': + return this.mainArea.add(config as any); + case 'bottomArea': case 'bottom': + return this.bottomArea.add(config as any); + case 'leftFixedArea': + return this.leftFixedArea.add(config as any); + case 'leftFloatArea': + return this.leftFloatArea.add(config as any); + case 'stages': + return this.stages.add(config as any); + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/stage.ts b/packages/vision-polyfill/src/skeleton/stage.ts new file mode 100644 index 000000000..f5c59a557 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/stage.ts @@ -0,0 +1,51 @@ +import Widget from './widget'; +import { Skeleton } from './skeleton'; +import { WidgetConfig } from './types'; + +export interface StageConfig extends WidgetConfig { + isRoot?: boolean; +} + +export class Stage extends Widget { + readonly isRoot: boolean; + private previous?: Stage; + private refer?: { + stage?: Stage; + direction?: 'right' | 'left'; + }; + + constructor(skeleton: Skeleton, config: StageConfig) { + super(skeleton, config); + this.isRoot = config.isRoot || false; + } + + setPrevious(stage: Stage) { + this.previous = stage; + } + + getPrevious() { + return this.previous; + } + + hasBack(): boolean { + return this.previous && !this.isRoot ? true : false; + } + + setRefer(stage: Stage, direction: 'right' | 'left') { + this.refer = { stage, direction }; + } + + setReferRight(stage: Stage) { + this.setRefer(stage, 'right'); + } + + setReferLeft(stage: Stage) { + this.setRefer(stage, 'left'); + } + + getRefer() { + const refer = this.refer; + this.refer = undefined; + return refer; + } +} diff --git a/packages/vision-polyfill/src/skeleton/theme.less b/packages/vision-polyfill/src/skeleton/theme.less new file mode 100644 index 000000000..5171884c0 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/theme.less @@ -0,0 +1,60 @@ +@import '~@ali/ve-less-variables/index.less'; + +/* + * Theme Colors + * + * 乐高设计器的主要主题色变量 + */ +:root { + --color-brand: @brand-color-1; + --color-brand-light: @brand-color-2; + --color-brand-dark: @brand-color-3; + + --color-canvas-background: @normal-alpha-8; + + --color-icon-normal: @normal-alpha-4; + --color-icon-hover: @normal-alpha-3; + --color-icon-active: @brand-color-1; + --color-icon-reverse: @white-alpha-1; + + --color-line-normal: @normal-alpha-7; + --color-line-darken: darken(@normal-alpha-7, 10%); + + --color-title: @dark-alpha-2; + --color-text: @dark-alpha-3; + --color-text-dark: darken(@dark-alpha-3, 10%); + --color-text-light: lighten(@dark-alpha-3, 10%); + --color-text-reverse: @white-alpha-2; + --color-text-regular: @normal-alpha-2; + + --color-field-label: @dark-alpha-4; + --color-field-text: @dark-alpha-3; + --color-field-placeholder: @normal-alpha-5; + --color-field-border: @normal-alpha-5; + --color-field-border-hover: @normal-alpha-4; + --color-field-border-active: @normal-alpha-3; + --color-field-background: @white-alpha-1; + + --color-function-success: @brand-success; + --color-function-success-dark: darken(@brand-success, 10%); + --color-function-success-light: lighten(@brand-success, 10%); + --color-function-warning: @brand-warning; + --color-function-warning-dark: darken(@brand-warning, 10%); + --color-function-warning-light: lighten(@brand-warning, 10%); + --color-function-information: @brand-link-hover; + --color-function-information-dark: darken(@brand-link-hover, 10%); + --color-function-information-light: lighten(@brand-link-hover, 10%); + --color-function-error: @brand-danger; + --color-function-error-dark: darken(@brand-danger, 10%); + --color-function-error-light: lighten(@brand-danger, 10%); + + --color-pane-background: @white-alpha-1; + --color-block-background-normal: @white-alpha-1; + --color-block-background-light: @normal-alpha-9; + --color-block-background-shallow: @normal-alpha-8; + --color-block-background-dark: @normal-alpha-7; + --color-block-background-disabled: @normal-alpha-6; + --color-block-background-deep-dark: @normal-5; + --color-layer-mask-background: @dark-alpha-7; + --color-layer-tooltip-background: rgba(44,47,51,0.8); +} diff --git a/packages/vision-polyfill/src/skeleton/toolbar.tsx b/packages/vision-polyfill/src/skeleton/toolbar.tsx new file mode 100644 index 000000000..7a0c7561a --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/toolbar.tsx @@ -0,0 +1,49 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-globals'; +import Area from './area'; + +@observer +export default class Toolbar extends Component<{ area: Area }> { + render() { + const { area } = this.props; + if (area.isEmpty()) { + return null; + } + return ( +
+ +
+ ); + } +} + +@observer +class Contents extends Component<{ area: Area }> { + render() { + const { area } = this.props; + const left: any[] = []; + const center: any[] = []; + const right: any[] = []; + area.container.items.forEach((item) => { + if (item.align === 'center') { + center.push(item.content); + } else if (item.align === 'right') { + right.push(item.content); + } else { + left.push(item.content); + } + }); + return ( + +
{left}
+
{center}
+
{right}
+
+ ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/top-area.tsx b/packages/vision-polyfill/src/skeleton/top-area.tsx new file mode 100644 index 000000000..2b39b1fbb --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/top-area.tsx @@ -0,0 +1,44 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-globals'; +import Area from './area'; + +@observer +export default class TopArea extends Component<{ area: Area }> { + render() { + const { area } = this.props; + return ( +
+ +
+ ); + } +} + +@observer +class Contents extends Component<{ area: Area }> { + render() { + const { area } = this.props; + const left: any[] = []; + const center: any[] = []; + const right: any[] = []; + area.container.items.forEach(item => { + if (item.align === 'center') { + center.push(item.content); + } else if (item.align === 'left') { + right.push(item.content); + } else { + left.push(item.content); + } + }); + return ( + +
{left}
+
{center}
+
{right}
+
+ ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/types.ts b/packages/vision-polyfill/src/skeleton/types.ts new file mode 100644 index 000000000..4825a827e --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/types.ts @@ -0,0 +1,104 @@ +import { ReactElement, ComponentType } from 'react'; +import { TitleContent, IconType, I18nData } from '@ali/lowcode-globals'; + +export interface IWidgetBaseConfig { + type: string; + name: string; + area?: string; // 停靠位置, 默认 float, 如果添加非固定区, + props?: object; + content?: any; + contentProps?: object; + // index?: number; + [extra: string]: any; +} + +export interface WidgetConfig extends IWidgetBaseConfig { + type: "Widget"; + name: string; // as pluginKey + props?: { + align?: "left" | "right" | "bottom" | "center" | "top"; + }; + content?: string | ReactElement | ComponentType; // children +} + +export function isWidgetConfig(obj: any): obj is WidgetConfig { + return obj && obj.type === "Custom"; +} + +export interface DockProps { + title?: TitleContent; + icon?: IconType; + size?: 'small' | 'medium' | 'large'; + className?: string; + description?: string | I18nData; + onClick?: () => void; +} + +export interface IDockBaseConfig extends IWidgetBaseConfig { + props?: DockProps & { + align?: "left" | "right" | "bottom" | "center" | "top"; + }; +} + +export interface DockConfig extends IDockBaseConfig { + type: "Dock"; + content?: string | ReactElement | ComponentType; +} + +export function isDockConfig(obj: any): obj is DockConfig { + return obj && /Dock$/.test(obj.type); +} + +// 按钮弹窗扩展 +export interface DialogDockConfig extends IDockBaseConfig { + type: "DialogDock"; + dialogProps?: { + title?: TitleContent; + [key: string]: any; + }; +} + +export function isDialogDockConfig(obj: any): obj is DialogDockConfig { + return obj && obj.type === 'DialogDock'; +} + +// 窗格扩展 +export interface PanelConfig extends IWidgetBaseConfig { + type: "Panel"; + content?: string | ReactElement | ComponentType | PanelConfig[]; // as children + props?: PanelProps; +} + +export function isPanelConfig(obj: any): obj is PanelConfig { + return obj && obj.type === 'Panel'; +} + +export type HelpTipConfig = string | { url?: string; content?: string | ReactElement }; + +export interface PanelProps { + title?: TitleContent; + icon?: any; // 冗余字段 + description?: string | I18nData; + hideTitleBar?: boolean; // panel.props 兼容,不暴露 + help?: HelpTipConfig; // 显示问号帮助 + width?: number; // panel.props + height?: number; // panel.props + maxWidth?: number; // panel.props + maxHeight?: number; // panel.props + onInit?: () => any; + onDestroy?: () => any; + shortcut?: string; // 只有在特定位置,可触发 toggle show +} + +export interface PanelDockConfig extends IDockBaseConfig { + type: "PanelDock"; + panelName?: string; + panelProps?: PanelProps & { + area?: string; + }; + content?: string | ReactElement | ComponentType | PanelConfig[]; // content for pane +} + +export function isPanelDockConfig(obj: any): obj is PanelDockConfig { + return obj && obj.type === 'PanelDock'; +} diff --git a/packages/vision-polyfill/src/skeleton/utils.ts b/packages/vision-polyfill/src/skeleton/utils.ts new file mode 100644 index 000000000..2f665a0bb --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/utils.ts @@ -0,0 +1,28 @@ +import { IconType, TitleContent, I18nData, isI18nData } from '@ali/lowcode-globals'; +import { isValidElement } from 'react'; + +export function composeTitle(title?: TitleContent, icon?: IconType, tip?: string | I18nData) { + if (!title) { + title = {}; + if (!icon) { + title.label = tip; + tip = undefined; + } + } + if (icon || tip) { + if (typeof title !== 'object' || isValidElement(title) || isI18nData(title)) { + title = { + label: title, + icon, + tip, + }; + } else { + title = { + ...title, + icon, + tip + }; + } + } + return title; +} diff --git a/packages/vision-polyfill/src/skeleton/widget-container.ts b/packages/vision-polyfill/src/skeleton/widget-container.ts new file mode 100644 index 000000000..b34827ae7 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget-container.ts @@ -0,0 +1,134 @@ +import { obx, hasOwnProperty, computed } from '@ali/lowcode-globals'; +import { isPanel } from './panel'; +export interface WidgetItem { + name: string; +} + +export interface Activeable { + setActive(flag: boolean): void; +} + +function isActiveable(obj: any): obj is Activeable { + return obj && obj.setActive; +} + +export default class WidgetContainer { + @obx.val items: T[] = []; + private maps: { [name: string]: T } = {}; + @obx.ref private _current: T & Activeable | null = null; + + get current() { + return this._current; + } + + constructor( + readonly name: string, + private handle: (item: T | G) => T, + private exclusive: boolean = false, + private checkVisible: () => boolean = () => true, + private defaultSetCurrent: boolean = false, + ) {} + + @computed get visible() { + return this.checkVisible(); + } + + active(nameOrItem?: T | string | null) { + let item: any = nameOrItem; + if (nameOrItem && typeof nameOrItem === 'string') { + item = this.get(nameOrItem); + } + if (!isActiveable(nameOrItem)) { + item = null; + } + + if (this.exclusive) { + if (this._current === item) { + return; + } + if (this._current) { + this._current.setActive(false); + } + this._current = item; + } + + if (item) { + item.setActive(true); + } + } + + unactive(nameOrItem?: T | string | null) { + let item: any = nameOrItem; + if (nameOrItem && typeof nameOrItem === 'string') { + item = this.get(nameOrItem); + } + if (!isActiveable(nameOrItem)) { + item = null; + } + if (this._current === item) { + this._current = null; + } + if (item) { + item.setActive(false); + } + } + + add(item: T | G): T { + item = this.handle(item); + const origin = this.get(item.name); + if (origin === item) { + return origin; + } + const i = origin ? this.items.indexOf(origin) : -1; + if (i > -1) { + this.items[i] = item; + } else { + this.items.push(item); + } + this.maps[item.name] = item; + if (isPanel(item)) { + item.setParent(this); + } + if (this.defaultSetCurrent) { + if (!this._current) { + this.active(item); + } + } + return item; + } + + get(name: string): T | null { + return this.maps[name] || null; + } + + getAt(index: number): T | null { + return this.items[index] || null; + } + + has(name: string): boolean { + return hasOwnProperty(this.maps, name); + } + + indexOf(item: T): number { + return this.items.indexOf(item); + } + + /** + * return indexOf the deletion + */ + remove(item: string | T): number { + const thing = typeof item === 'string' ? this.get(item) : item; + if (!thing) { + return -1; + } + const i = this.items.indexOf(thing); + if (i > -1) { + this.items.splice(i, 1); + } + delete this.maps[thing.name]; + if (thing === this.current) { + this._current = null; + } + return i; + } +} diff --git a/packages/vision-polyfill/src/skeleton/widget-views.tsx b/packages/vision-polyfill/src/skeleton/widget-views.tsx new file mode 100644 index 000000000..af5d2364c --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget-views.tsx @@ -0,0 +1,143 @@ +import { Component, ReactElement } from 'react'; +import classNames from 'classnames'; +import { Title, observer } from '@ali/lowcode-globals'; +import { DockProps } from './types'; +import PanelDock from './panel-dock'; +import { composeTitle } from './utils'; +import WidgetContainer from './widget-container'; +import Panel from './panel'; +import Widget from './widget'; + +export function DockView({ title, icon, description, size, className, onClick }: DockProps) { + return ( + + ); +} + +@observer +export class PanelDockView extends Component<DockProps & { dock: PanelDock }> { + render() { + const { dock, className, onClick, ...props } = this.props; + return DockView({ + ...props, + className: classNames(className, { + actived: dock.actived, + }), + onClick: () => { + onClick && onClick(); + dock.toggle(); + }, + }); + } +} + +export class DialogDockView extends Component { + +} + +export class PanelView extends Component<{ panel: Panel }> { + shouldComponentUpdate() { + return false; + } + render() { + const { panel } = this.props; + return ( + <div className="lc-panel"> + <PanelTitle panel={panel} /> + <div className="lc-pane-body">{panel.body}</div> + </div> + ); + } +} + +@observer +export class TabsPanelView extends Component<{ container: WidgetContainer<Panel> }> { + render() { + const { container } = this.props; + const titles: ReactElement[] = []; + const contents: ReactElement[] = []; + container.items.forEach((item: any) => { + titles.push(<PanelTitle key={item.id} panel={item} className="lc-tab-title" />); + contents.push(<PanelWrapper key={item.id} panel={item} />); + }); + + return ( + <div className="lc-tabs"> + <div + className="lc-tabs-title" + onClick={(e) => { + const shell = e.currentTarget; + const t = e.target as Element; + let elt = shell.firstElementChild; + while (elt) { + if (elt.contains(t)) { + break; + } + elt = elt.nextElementSibling; + } + if (elt) { + container.active((elt as any).dataset.name); + } + }} + > + {titles} + </div> + <div className="lc-tabs-content">{contents}</div> + </div> + ); + } +} + +@observer +class PanelTitle extends Component<{ panel: Panel; className?: string }> { + render() { + const { panel, className } = this.props; + return ( + <div + className={classNames('lc-panel-title', className, { + actived: panel.actived, + })} + data-name={panel.name} + > + <Title title={panel.title || panel.name} /> + {/*pane.help ? <HelpTip tip={panel.help} /> : null*/} + </div> + ); + } +} + +@observer +export class PanelWrapper extends Component<{ panel: Panel }> { + render() { + const { panel } = this.props; + if (!panel.inited) { + return null; + } + return ( + <div + className={classNames('lc-panel-wrapper', { + hidden: !panel.actived, + })} + > + {panel.body} + </div> + ); + } +} + +@observer +export class WidgetWrapper extends Component<{ widget: Widget }> { + render() { + const { widget } = this.props; + if (!widget.visible) { + return null; + } + return widget.body; + } +} diff --git a/packages/vision-polyfill/src/skeleton/widget.ts b/packages/vision-polyfill/src/skeleton/widget.ts new file mode 100644 index 000000000..f28aba2d9 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget.ts @@ -0,0 +1,94 @@ +import { ReactNode, createElement } from 'react'; +import { createContent, uniqueId, obx } from '@ali/lowcode-globals'; +import { WidgetConfig } from './types'; +import { Skeleton } from './skeleton'; +import { WidgetWrapper } from './widget-views'; + +export interface IWidget { + readonly name: string; + readonly content: any; + readonly align?: string; + readonly isWidget: true; + + getName(): string; + getContent(): any; + show(): void; + hide(): void; +} + +export default class Widget implements IWidget { + readonly isWidget = true; + readonly id = uniqueId('widget'); + readonly name: string; + readonly align?: string; + + @obx.ref private _visible: boolean = true; + get visible(): boolean { + return this._visible; + } + + @obx.ref inited: boolean = false; + private _body: ReactNode; + get body() { + if (this.inited) { + return this._body; + } + this.inited = true; + const { content, contentProps } = this.config; + this._body = createContent(content, { + ...contentProps, + editor: this.skeleton.editor, + }); + return this._body; + } + + get content() { + return createElement(WidgetWrapper, { + widget: this, + key: this.id, + }); + } + + constructor(readonly skeleton: Skeleton, private config: WidgetConfig) { + const { props = {}, name } = config; + this.name = name; + this.align = props.align; + } + + getName() { + return this.name; + } + + getContent() { + return this.content; + } + + hide() { + this.setVisible(false); + } + + show() { + this.setVisible(true); + } + + setVisible(flag: boolean) { + if (flag === this._visible) { + return; + } + if (flag) { + this._visible = true; + } else if (this.inited) { + this._visible = false; + } + } + + toggle() { + this.setVisible(!this._visible); + } +} + +export function isWidget(obj: any): obj is IWidget { + return obj && obj.isWidget; +} + + diff --git a/packages/vision-polyfill/src/skeleton/workbench.less b/packages/vision-polyfill/src/skeleton/workbench.less new file mode 100644 index 000000000..3ac20e84f --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/workbench.less @@ -0,0 +1,261 @@ +@import "./theme.less"; + +:root { + --font-family: @font-family; + --font-size-label: @fontSize-4; + --font-size-text: @fontSize-5; + --font-size-btn-large: @fontSize-3; + --font-size-btn-medium: @fontSize-4; + --font-size-btn-small: @fontSize-5; + + --global-border-radius: @global-border-radius; + --input-border-radius: @input-border-radius; + --popup-border-radius: @popup-border-radius; + + --left-area-width: 46px; + --right-area-width: 300px; + --top-area-height: 48px; + --toolbar-height: 32px; + --dock-pane-width: 372px; +} + + +@media (min-width: 1860px) { + :root { + --right-area-width: 400px; + --dock-pane-width: 452px; + } +} + +html, +body { + height: 100%; + overflow: hidden; + padding: 0; + margin: 0; + position: relative; + font-family: var(--font-family); + font-size: var(--font-size-text); + color: var(--color-text); + background-color:#EDEFF3; +} + +* { + box-sizing: border-box; +} + + +.my-pane { + width: 100%; + height: 100%; + position: relative; + .pane-title { + // height: var(--pane-title-height); + background-color: var(--pane-title-bg-color); + display: flex; + align-items: center; + padding: 0 15px; + .my-help-tip { + margin-left: 4px; + } + } + + .pane-body { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: auto; + .my-tabs { + width: 100%; + height: 100%; + position: relative; + .tabs-title { + display: flex; + height: var(--pane-title-height); + > .tab-title { + cursor: pointer; + padding: 0; + flex: 1; + min-width: 0; + justify-content: center; + border-bottom: 2px solid transparent; + &.actived { + cursor: default; + color: var(--color-text-avtived); + border-bottom-color: #3896ee; + } + } + } + .tabs-content { + position: absolute; + top: var(--pane-title-height); + bottom: 0; + left: 0; + right: 0; + height: calc(100% - var(--pane-title-height)); + overflow: hidden; + } + } + } + + &.titled > .pane-body { + top: var(--pane-title-height); + } +} +.lc-panel-wrapper { + height: 100%; + width: 100%; + overflow: auto; + &.hidden { + display: none; + } + .pane-title { + height: var(--pane-title-height); + background-color: var(--pane-title-bg-color); + display: flex; + align-items: center; + padding: 0 15px; + .my-help-tip { + margin-left: 4px; + } + } + .my-tabs { + width: 100%; + height: 100%; + position: relative; + .tabs-title { + display: flex; + height: var(--pane-title-height); + margin-right: 30px; + > .tab-title { + cursor: pointer; + padding: 0; + flex: 1; + min-width: 0; + justify-content: center; + border-bottom: 2px solid transparent; + &.actived { + cursor: default; + color: var(--color-text-avtived); + border-bottom-color: #3896ee; + } + } + } + .tabs-content { + position: absolute; + top: var(--pane-title-height); + bottom: 0; + left: 0; + right: 0; + height: calc(100% - var(--pane-title-height)); + overflow: hidden; + } + } +} + +.my-dock { + padding: 0px 10px; + cursor: pointer; + align-self: stretch; + display: flex; + align-items: center; + .my-title-label { + user-select: none; + } + &.actived, &:hover { + background-color: var(--pane-title-bg-color); + .my-title { + color: var(--color-actived); + } + } +} + + + + +.lc-workbench { + height: 100%; + display: flex; + flex-direction: column; + background-color: #EDEFF3; + .lc-top-area { + height: var(--top-area-height); + background-color: var(--color-pane-background); + width: 100%; + display: flex; + margin-bottom: 2px; + } + .lc-workbench-body { + flex: 1; + display: flex; + position: relative; + .lc-left-float-pane { + position: absolute; + top: 0; + bottom: 0; + width: var(--dock-pane-width); + left: calc(var(--left-area-width) + 1px); + z-index: 20; + display: none; + &.lc-area-visible { + display: block; + } + } + .lc-left-area { + height: 100%; + width: var(--left-area-width); + background-color: var(--color-pane-background); + display: none; + &.lc-area-visible { + display: flex; + } + } + .lc-left-fixed-pane { + width: var(--dock-pane-width); + background-color: var(--color-pane-background); + height: 100%; + display: none; + &.lc-area-visible { + display: block; + } + } + .lc-left-area.lc-area-visible ~ .lc-left-fixed-pane { + margin-left: 1px; + } + .lc-left-area.lc-area-visible ~ .lc-workbench-center { + margin-left: 2px; + } + .lc-workbench-center { + flex: 1; + display: flex; + flex-direction: column; + .lc-toolbar { + height: var(--toolbar-height); + background-color: var(--color-pane-background); + } + .lc-main-area { + flex: 1; + } + .lc-bottom-area { + height: var(--bottom-area-height); + background-color: var(--color-pane-background); + display: none; + &.lc-area-visible { + display: block; + } + } + } + .lc-right-area { + height: 100%; + width: var(--right-area-width); + background-color: var(--color-pane-background); + display: none; + margin-left: 2px; + &.lc-area-visible { + display: block; + } + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/workbench.tsx b/packages/vision-polyfill/src/skeleton/workbench.tsx new file mode 100644 index 000000000..aa92a2ae6 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/workbench.tsx @@ -0,0 +1,40 @@ +import { Component } from 'react'; +import { TipContainer, observer } from '@ali/lowcode-globals'; +import { Skeleton } from './skeleton'; +import TopArea from './top-area'; +import LeftArea from './left-area'; +import LeftFixedPane from './left-fixed-pane'; +import LeftFloatPane from './left-float-pane'; +import Toolbar from './toolbar'; +import MainArea from './main-area'; +import BottomArea from './bottom-area'; +import RightArea from './right-area'; +import './workbench.less'; + +@observer +export class VisionWorkbench extends Component<{ skeleton: Skeleton}> { + shouldComponentUpdate() { + return false; + } + + render() { + const { skeleton } = this.props; + return ( + <div className="lc-workbench"> + <TopArea area={skeleton.topArea} /> + <div className="lc-workbench-body"> + <LeftArea area={skeleton.leftArea} /> + <LeftFloatPane area={skeleton.leftFloatArea} /> + <LeftFixedPane area={skeleton.leftFixedArea} /> + <div className="lc-workbench-center"> + <Toolbar area={skeleton.toolbar} /> + <MainArea area={skeleton.mainArea} /> + <BottomArea area={skeleton.bottomArea} /> + </div> + <RightArea area={skeleton.rightArea} /> + </div> + <TipContainer /> + </div> + ); + } +} diff --git a/packages/vision-polyfill/src/vision.ts b/packages/vision-polyfill/src/vision.ts index ac016b299..a6a186835 100644 --- a/packages/vision-polyfill/src/vision.ts +++ b/packages/vision-polyfill/src/vision.ts @@ -6,8 +6,9 @@ import { createElement } from 'react'; import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS } from './const'; import Bus from './bus'; import Symbols from './symbols'; -import Skeleton from '@ali/lowcode-editor-skeleton'; -import editor from './editor'; +import { editor, skeleton } from './editor'; +import { VisionWorkbench } from './skeleton/workbench'; +import Panes from './panes'; function init(container?: Element) { if (!container) { @@ -16,9 +17,12 @@ function init(container?: Element) { } container.id = 'engine'; - render(createElement(Skeleton, { - editor, - }), container); + render( + createElement(VisionWorkbench, { + skeleton, + }), + container, + ); } const ui = { @@ -51,4 +55,5 @@ export { */ init, ui, + Panes, };