diff --git a/packages/demo/package.json b/packages/demo/package.json index df84e721f..9ff3918c5 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -20,21 +20,22 @@ "@ali/lowcode-plugin-sample-logo": "^0.8.8", "@ali/lowcode-plugin-sample-preview": "^0.8.11", "@ali/lowcode-plugin-settings-pane": "^0.8.8", - "@ali/lowcode-plugin-source-editor": "^0.8.5", "@ali/lowcode-plugin-undo-redo": "^0.8.9", "@ali/lowcode-plugin-variable-bind-dialog": "^0.8.7", "@ali/lowcode-plugin-zh-en": "^0.8.11", "@ali/lowcode-react-renderer": "^0.8.5", "@ali/lowcode-runtime": "^0.8.13", - "@ali/lowcode-setters": "^0.8.11", "@ali/lowcode-utils": "^0.8.2", - "@ali/vs-variable-setter": "^3.1.0", "@ali/ve-action-pane": "^4.7.0-beta.0", "@ali/ve-datapool-pane": "^6.4.3", "@ali/ve-i18n-manage-pane": "^4.3.0", "@ali/ve-i18n-pane": "^4.0.0-beta.0", "@ali/ve-trunk-pane": "^5.1.0-beta.14", + "@ali/vs-variable-setter": "^3.1.0", "@ali/vu-legao-design-fetch-context": "^1.0.3", + "@ali/ve-page-history": "1.2.0", + "@ali/ve-history-pane": "4.0.0", + "@ali/ve-page-history-pane": "^5.0.0-beta.0", "@alifd/next": "^1.19.12", "@alife/theme-lowcode-dark": "^0.1.0", "@alife/theme-lowcode-light": "^0.1.0", diff --git a/packages/demo/src/editor/config/components.js b/packages/demo/src/editor/config/components.js index e844c44ed..95831f033 100644 --- a/packages/demo/src/editor/config/components.js +++ b/packages/demo/src/editor/config/components.js @@ -9,7 +9,7 @@ import settingsPane from '@ali/lowcode-plugin-settings-pane'; import designer from '@ali/lowcode-plugin-designer'; import eventBindDialog from '@ali/lowcode-plugin-event-bind-dialog'; import variableBindDialog from '@ali/lowcode-plugin-variable-bind-dialog'; -import sourceEditor from '@ali/lowcode-plugin-source-editor'; +// import sourceEditor from '@ali/lowcode-plugin-source-editor'; export default { LowcodeSkeleton, logo, @@ -22,5 +22,5 @@ export default { designer, eventBindDialog, variableBindDialog, - sourceEditor + // sourceEditor }; diff --git a/packages/demo/src/editor/index.tsx b/packages/demo/src/editor/index.tsx index ab2d9526f..48c99732c 100644 --- a/packages/demo/src/editor/index.tsx +++ b/packages/demo/src/editor/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { registerSetters } from '@ali/lowcode-setters'; +// import { registerSetters } from '@ali/lowcode-setters'; import config from './config/skeleton'; import components from './config/components'; import utils from './config/utils'; @@ -8,7 +8,7 @@ import utils from './config/utils'; import './global.scss'; import './config/theme.scss'; -registerSetters(); +// registerSetters(); const Skeleton = components.LowcodeSkeleton; const LCE_CONTAINER = document.getElementById('lce-container'); diff --git a/packages/demo/src/vision/index.ts b/packages/demo/src/vision/index.ts index 5407a3b27..47a37895d 100644 --- a/packages/demo/src/vision/index.ts +++ b/packages/demo/src/vision/index.ts @@ -2,8 +2,12 @@ import { createElement } from 'react'; import { Button } from '@alifd/next'; import Engine, { Panes } from '@ali/visualengine'; +import { ActionUtil as actionUtil } from '@ali/visualengine-utils'; import getTrunkPane from '@ali/ve-trunk-pane'; import DatapoolPane from '@ali/ve-datapool-pane'; +import PageHistoryManager from '@ali/ve-page-history'; +import HistoryPane from '@ali/ve-history-pane'; +import PageHistoryPane from '@ali/ve-page-history-pane'; // import I18nPane from '@ali/ve-i18n-pane'; import I18nManagePane from '@ali/ve-i18n-manage-pane'; import ActionPane from '@ali/ve-action-pane'; @@ -278,6 +282,43 @@ function initI18nPane() { // 动作面板 function initActionPane() { + actionUtil.setActions({ + module: { + compiled: "'use strict';\n\nexports.__esModule = true;\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nexports.submit = submit;\nexports.onLoadData = onLoadData;\nexports.add = add;\nexports.edit = edit;\nexports.del = del;\nexports.search = search;\nexports.reset = reset;\n/**\n* 点击弹框的“确认”\n*/\nfunction submit() {\n var _this = this;\n\n this.$('form').submit(function (data, error) {\n if (data) {\n _this.dataSourceMap['table_submit'].load(data).then(function (res) {\n _this.utils.toast({\n type: 'success',\n title: '提交成功'\n });\n _this.$('dialog').hide();\n _this.dataSourceMap['table_list'].load();\n }).catch(function () {\n _this.utils.toast({\n type: 'error',\n title: '提交失败'\n });\n });\n }\n });\n}\n\n/**\n* tablePc onLoadData\n* @param currentPage 当前页码\n* @param pageSize 每页显示条数\n* @param searchKey 搜索关键字\n* @param orderColumn 排序列\n* @param orderType 排序方式(desc,asc)\n* @param from 触发来源(order,search,pagination)\n*/\nfunction onLoadData(currentPage, pageSize, searchKey, orderColumn, orderType, from) {\n var tableParams = {\n currentPage: from === 'search' ? 1 : currentPage,\n pageSize: pageSize,\n searchKey: searchKey,\n orderColumn: orderColumn,\n orderType: orderType\n };\n this.setState({ tableParams: tableParams });\n}\n\n// 点击新增\nfunction add() {\n this.setState({\n formData: null\n });\n this.$('dialog').show();\n}\n\n// 点击编辑\nfunction edit(rowData) {\n this.setState({\n formData: rowData\n });\n this.$('dialog').show();\n}\n\n// 点击删除\nfunction del(rowData) {\n var _this2 = this;\n\n this.utils.dialog({\n method: 'confirm',\n title: '提示',\n content: '确认删除该条目吗?',\n onOk: function onOk() {\n _this2.dataSourceMap['table_delete'].load({ id: rowData.id }).then(function () {\n _this2.utils.toast({\n type: 'success',\n title: '删除成功'\n });\n _this2.dataSourceMap['table_list'].load();\n }).catch(function () {\n _this2.utils.toast({\n type: 'error',\n title: '删除失败'\n });\n });\n }\n });\n}\n\n/**\n* button onClick\n*/\nfunction search() {\n var filterData = this.$('filter').getValue();\n this.setState({\n filterData: filterData,\n tableParams: _extends({}, this.state.tableParams, {\n time: Date.now(),\n currentPage: 1\n })\n });\n}\n\n/**\n* button onClick\n*/\nfunction reset() {\n this.$('filter').reset();\n this.setState({\n filterData: {},\n tableParams: _extends({}, this.state.tableParams, {\n time: Date.now(),\n currentPage: 1\n })\n });\n}", + source: "/**\n* 点击弹框的“确认”\n*/\nexport function submit() {\n this.$('form').submit((data, error) => {\n if (data) {\n this.dataSourceMap['table_submit'].load(data).then((res) => {\n this.utils.toast({\n type: 'success',\n title: '提交成功'\n });\n this.$('dialog').hide();\n this.dataSourceMap['table_list'].load();\n }).catch(()=>{\n this.utils.toast({\n type: 'error',\n title: '提交失败'\n });\n })\n }\n })\n}\n\n/**\n* tablePc onLoadData\n* @param currentPage 当前页码\n* @param pageSize 每页显示条数\n* @param searchKey 搜索关键字\n* @param orderColumn 排序列\n* @param orderType 排序方式(desc,asc)\n* @param from 触发来源(order,search,pagination)\n*/\nexport function onLoadData(currentPage, pageSize, searchKey, orderColumn, orderType, from) {\n const tableParams = {\n currentPage: from === 'search' ? 1 : currentPage,\n pageSize,\n searchKey,\n orderColumn,\n orderType\n };\n this.setState({ tableParams });\n}\n\n// 点击新增\nexport function add() {\n this.setState({\n formData: null,\n });\n this.$('dialog').show();\n}\n\n\n// 点击编辑\nexport function edit(rowData) {\n this.setState({\n formData: rowData\n });\n this.$('dialog').show();\n}\n\n// 点击删除\nexport function del(rowData) {\n this.utils.dialog({\n method: 'confirm',\n title: '提示',\n content: '确认删除该条目吗?',\n onOk: () => {\n this.dataSourceMap['table_delete'].load({ id: rowData.id }).then(() => {\n this.utils.toast({\n type: 'success',\n title: '删除成功'\n });\n this.dataSourceMap['table_list'].load();\n }).catch(()=>{\n this.utils.toast({\n type: 'error',\n title: '删除失败'\n });\n })\n }\n })\n}\n\n/**\n* button onClick\n*/\nexport function search(){\n const filterData = this.$('filter').getValue();\n this.setState({\n filterData,\n tableParams: {\n ...this.state.tableParams,\n time: Date.now(),\n currentPage: 1\n }\n });\n}\n\n/**\n* button onClick\n*/\nexport function reset(){\n this.$('filter').reset();\n this.setState({\n filterData: {},\n tableParams: {\n ...this.state.tableParams,\n time: Date.now(),\n currentPage: 1\n }\n });\n}" + }, + type: "FUNCTION", + list: [ + { + "id": "submit", + "title": "submit" + }, + { + "id": "onLoadData", + "title": "onLoadData" + }, + { + "id": "add", + "title": "add" + }, + { + "id": "edit", + "title": "edit" + }, + { + "id": "del", + "title": "del" + }, + { + "id": "search", + "title": "search" + }, + { + "id": "reset", + "title": "reset" + } + ] + }); const props = { enableGlobalJS: false, enableVsCodeEdit: false, @@ -314,6 +355,63 @@ function initActionPane() { // return props; // }; +// 操作历史与页面历史面板 +function initHistoryPane() { + // let historyConfigs = {getDesignerModuleConfigs( + // this.designerConfigs, + // 'history', + // )}; + let historyConfigs = { + enableRedoAndUndo: true, + enablePageHistory: true, + };; + + const isDemoMode = false; + const isEnvSupportsHistoryPane = true; + const historyManager = PageHistoryManager.getManager(); + + console.log('PageHistoryManager', historyManager); + console.log('PageHistoryManager.onOpenPane', historyManager.onOpenPane); + // 历史撤销、重做以及唤起页面历史按钮 + if (typeof HistoryPane === 'function') { + Panes.add(HistoryPane, { + props : { + showPageHistory: + isEnvSupportsHistoryPane + // && this.app.isForm() + && !isDemoMode, + historyManager, + historyConfigs, + index: -940, + } + }); + } else { + Panes.add(HistoryPane, { + index: -940, + }); + } + + // 页面历史 UI 面板 + if ( + PageHistoryPane + && !isDemoMode + && isEnvSupportsHistoryPane + ) { + Panes.add(PageHistoryPane, { + props : { + historyManager: { + historyManager, + app: { + + } + }, + index: -940, + }, + }); + } +} + + async function init() { Engine.Env.setEnv('RE_VERSION', '7.2.0'); Engine.Env.setSupportFeatures({ @@ -331,6 +429,7 @@ async function init() { // debugger // Prototype.addGlobalPropsReducer(replaceFuncProp); // debugger + initHistoryPane(); Engine.init(); } init(); diff --git a/packages/demo/src/vision/module.d.ts b/packages/demo/src/vision/module.d.ts index d7ac3cb98..edbe109b1 100644 --- a/packages/demo/src/vision/module.d.ts +++ b/packages/demo/src/vision/module.d.ts @@ -1,9 +1,12 @@ -declare module "@ali/visualengine"; -declare module "@ali/visualengine-utils"; -declare module "@ali/ve-trunk-pane"; -declare module "@ali/vs-variable-setter"; -declare module "@ali/ve-datapool-pane"; -declare module "@ali/ve-i18n-manage-pane"; -declare module "@ali/ve-action-pane"; -declare module "@ali/vu-legao-design-fetch-context"; +declare module '@ali/visualengine'; +declare module '@ali/visualengine-utils'; +declare module '@ali/ve-trunk-pane'; +declare module '@ali/vs-variable-setter'; +declare module '@ali/ve-datapool-pane'; +declare module '@ali/ve-history-pane'; +declare module '@ali/ve-page-history-pane'; +declare module '@ali/ve-page-history'; +declare module '@ali/ve-i18n-manage-pane'; +declare module '@ali/ve-action-pane'; +declare module '@ali/vu-legao-design-fetch-context'; // declare module "@ali/vu-function-parser"; diff --git a/packages/designer/src/designer/setting/utils.js b/packages/designer/src/designer/setting/utils.js index b8f5bbdcc..d9e7e389c 100644 --- a/packages/designer/src/designer/setting/utils.js +++ b/packages/designer/src/designer/setting/utils.js @@ -1,12 +1,16 @@ +// all this file for polyfill vision logic + +import { isValidElement } from 'react'; + function getHotterFromSetter(setter) { return setter && (setter.Hotter || (setter.type && setter.type.Hotter)) || []; // eslint-disable-line } function getTransducerFromSetter(setter) { return setter && ( - setter.transducer || setter.Transducer - || (setter.type && (setter.type.transducer || setter.type.Transducer)) - ) || null; // eslint-disable-line + setter.transducer || setter.Transducer + || (setter.type && (setter.type.transducer || setter.type.Transducer)) + ) || null; // eslint-disable-line } function combineTransducer(transducer, arr, context) { @@ -23,9 +27,22 @@ function combineTransducer(transducer, arr, context) { export class Transducer { constructor(context, config) { + let { setter } = config; + + // 1. validElement + // 2. SetterConfig + // 3. SetterConfig[] + if (Array.isArray(setter)) { + setter = setter[0]; + } else if (isValidElement(setter) && setter.type.displayName === 'MixedSetter') { + setter = setter.props.setters[0]; + } else if (typeof setter === 'object' && setter.componentName === 'MixedSetter') { + setter = setter.props.setters[0]; + } + this.setterTransducer = combineTransducer( - getTransducerFromSetter(config.setter), - getHotterFromSetter(config.setter), + getTransducerFromSetter(setter), + getHotterFromSetter(setter), context, ); this.context = context; diff --git a/packages/designer/src/document/document-model.ts b/packages/designer/src/document/document-model.ts index eb92fb1c7..ac739d625 100644 --- a/packages/designer/src/document/document-model.ts +++ b/packages/designer/src/document/document-model.ts @@ -12,8 +12,8 @@ import { uniqueId } from '@ali/lowcode-utils'; export type GetDataType = T extends undefined ? NodeType extends { - schema: infer R; - } + schema: infer R; + } ? R : any : T; @@ -465,6 +465,14 @@ export class DocumentModel { getRoot() { return this.rootNode; } + + /** + * 兼容vision + */ + getHistory(): History { + return this.history; + } + get root() { return this.rootNode; } diff --git a/packages/editor-skeleton/src/components/mixed-setter/index.tsx b/packages/editor-skeleton/src/components/mixed-setter/index.tsx index 67e020925..1adafc6eb 100644 --- a/packages/editor-skeleton/src/components/mixed-setter/index.tsx +++ b/packages/editor-skeleton/src/components/mixed-setter/index.tsx @@ -115,6 +115,8 @@ export default class MixedSetter extends Component<{ className?: string; }> { private setters = nomalizeSetters(this.props.setters); + // set name ,used in setting Transducer + static displayName = 'MixedSetter'; @obx.ref private used?: string; @computed private getCurrentSetter() { const { field } = this.props; diff --git a/packages/editor-skeleton/src/components/widget-views.tsx b/packages/editor-skeleton/src/components/widget-views.tsx index cdd646be7..13dad59f8 100644 --- a/packages/editor-skeleton/src/components/widget-views.tsx +++ b/packages/editor-skeleton/src/components/widget-views.tsx @@ -1,6 +1,7 @@ import { Component, ReactElement } from 'react'; +import { Icon } from '@alifd/next'; import classNames from 'classnames'; -import { Title, observer } from '@ali/lowcode-editor-core'; +import { Title, observer, Tip } from '@ali/lowcode-editor-core'; import { DockProps } from '../types'; import PanelDock from '../widget/panel-dock'; import { composeTitle } from '../widget/utils'; @@ -21,6 +22,25 @@ export function DockView({ title, icon, description, size, className, onClick }: ); } +function HelpTip({ tip }: any) { + if (tip && tip.url) { + return ( +
+ + + + {tip.content} +
+ ); + } + return ( +
+ + {tip.content} +
+ ) +} + @observer export class PanelDockView extends Component { componentDidMount() { @@ -196,7 +216,7 @@ class PanelTitle extends Component<{ panel: Panel; className?: string }> { data-name={panel.name} > - {/*pane.help ? <HelpTip tip={panel.help} /> : null*/} + {panel.help ? <HelpTip tip={panel.help} /> : null} </div> ); } diff --git a/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx b/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx index c5ea01b53..527b48b98 100644 --- a/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx +++ b/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx @@ -13,21 +13,26 @@ export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, P } render() { const { area } = this.props; + const hideTitleBar = area.current?.config.props?.hideTitleBar; return ( <div className={classNames('lc-left-fixed-pane', { 'lc-area-visible': area.visible, })} > - <Button - text - className="lc-pane-close" - onClick={() => { - area.setVisible(false); - }} - > - <Icon type="close" /> - </Button> + { + !hideTitleBar && ( + <Button + text + className="lc-pane-close" + onClick={() => { + area.setVisible(false); + }} + > + <Icon type="close" /> + </Button> + ) + } <Contents area={area} /> </div> ); diff --git a/packages/editor-skeleton/src/layouts/left-float-pane.tsx b/packages/editor-skeleton/src/layouts/left-float-pane.tsx index 84feacd6f..d9b32b22a 100644 --- a/packages/editor-skeleton/src/layouts/left-float-pane.tsx +++ b/packages/editor-skeleton/src/layouts/left-float-pane.tsx @@ -32,6 +32,7 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> // focusout remove focus // onEsc const width = area.current?.config.props?.width; + const hideTitleBar = area.current?.config.props?.hideTitleBar; const style = width ? { width } : undefined; @@ -42,15 +43,19 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> })} style={style} > - <Button - text - className="lc-pane-close" - onClick={() => { - area.setVisible(false); - }} - > - <Icon type="close" /> - </Button> + { + !hideTitleBar && ( + <Button + text + className="lc-pane-close" + onClick={() => { + area.setVisible(false); + }} + > + <Icon type="close" /> + </Button> + ) + } <Contents area={area} /> </div> ); diff --git a/packages/editor-skeleton/src/layouts/theme.less b/packages/editor-skeleton/src/layouts/theme.less index 5171884c0..0d4651b17 100644 --- a/packages/editor-skeleton/src/layouts/theme.less +++ b/packages/editor-skeleton/src/layouts/theme.less @@ -57,4 +57,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); + + --pane-title-bg-color: rgba(31,56,88,.04); } diff --git a/packages/editor-skeleton/src/layouts/workbench.less b/packages/editor-skeleton/src/layouts/workbench.less index 94d335a90..4efca072d 100644 --- a/packages/editor-skeleton/src/layouts/workbench.less +++ b/packages/editor-skeleton/src/layouts/workbench.less @@ -54,19 +54,25 @@ body { display: none; } .lc-panel-title { - height: 32px; - background-color: var(--pane-title-bg-color); + height: 38px; + font-size: 14px; + background-color: var(--pane-title-bg-color,rgba(31,56,88,.04)); display: flex; align-items: center; + justify-content: center; padding: 0 15px; + border-bottom: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); + .lc-help-tip { margin-left: 4px; + color: rgba(0,0,0,0.4); + cursor: pointer; } } .lc-panel-body { position: absolute; - top: 32px; + top: 38px; bottom: 0; left: 0; right: 0; diff --git a/packages/editor-skeleton/src/layouts/workbench.tsx b/packages/editor-skeleton/src/layouts/workbench.tsx index 857fb8540..af50045c4 100644 --- a/packages/editor-skeleton/src/layouts/workbench.tsx +++ b/packages/editor-skeleton/src/layouts/workbench.tsx @@ -1,5 +1,6 @@ import { Component } from 'react'; import { TipContainer, observer } from '@ali/lowcode-editor-core'; +import classNames from 'classnames'; import { Skeleton } from '../skeleton'; import TopArea from './top-area'; import LeftArea from './left-area'; @@ -12,15 +13,15 @@ import RightArea from './right-area'; import './workbench.less'; @observer -export class Workbench extends Component<{ skeleton: Skeleton}> { +export class Workbench extends Component<{ skeleton: Skeleton, className?: string }> { shouldComponentUpdate() { return false; } render() { - const { skeleton } = this.props; + const { skeleton, className } = this.props; return ( - <div className="lc-workbench"> + <div className={classNames('lc-workbench', className)}> <TopArea area={skeleton.topArea} /> <div className="lc-workbench-body"> <LeftArea area={skeleton.leftArea} /> diff --git a/packages/editor-skeleton/src/widget/panel.ts b/packages/editor-skeleton/src/widget/panel.ts index 019309370..d0336fe6c 100644 --- a/packages/editor-skeleton/src/widget/panel.ts +++ b/packages/editor-skeleton/src/widget/panel.ts @@ -163,15 +163,23 @@ export default class Panel implements IWidget { this.setActive(true); } - + /** + * @deprecated + */ getSupportedPositions() { return ['default']; } + /** + * @deprecated + */ getCurrentPosition() { return 'default'; } + /** + * @deprecated + */ setPosition(position: string) { } } diff --git a/packages/vision-preset/package.json b/packages/vision-preset/package.json index a99baaeab..2010ea9b0 100644 --- a/packages/vision-preset/package.json +++ b/packages/vision-preset/package.json @@ -20,7 +20,6 @@ "@ali/lowcode-plugin-outline-pane": "^0.8.12", "@ali/lowcode-plugin-undo-redo": "^0.8.9", "@ali/lowcode-plugin-zh-en": "^0.8.11", - "@ali/lowcode-setters": "^0.8.11", "@ali/ve-i18n-util": "^2.0.2", "@ali/ve-icons": "^4.1.9", "@ali/ve-less-variables": "2.0.3", diff --git a/packages/vision-preset/src/editor.ts b/packages/vision-preset/src/editor.ts index 2c965991e..96cc11e03 100644 --- a/packages/vision-preset/src/editor.ts +++ b/packages/vision-preset/src/editor.ts @@ -2,16 +2,16 @@ import { isJSBlock } from '@ali/lowcode-types'; import { isPlainObject } from '@ali/lowcode-utils'; import { globalContext, Editor } from '@ali/lowcode-editor-core'; import { Designer, TransformStage } from '@ali/lowcode-designer'; -import { registerSetters } from '@ali/lowcode-setters'; +// import { registerSetters } from '@ali/lowcode-setters'; import Outline from '@ali/lowcode-plugin-outline-pane'; import DesignerPlugin from '@ali/lowcode-plugin-designer'; import { Skeleton, SettingsPrimaryPane } from '@ali/lowcode-editor-skeleton'; import Preview from '@ali/lowcode-plugin-sample-preview'; -import SourceEditor from '@ali/lowcode-plugin-source-editor'; +// import SourceEditor from '@ali/lowcode-plugin-source-editor'; import { i18nReducer } from './i18n-reducer'; -registerSetters(); +// registerSetters(); export const editor = new Editor(); globalContext.register(editor, Editor); diff --git a/packages/vision-preset/src/field.tsx b/packages/vision-preset/src/field.tsx deleted file mode 100644 index 346ed36b1..000000000 --- a/packages/vision-preset/src/field.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Component, ReactNode } from 'react'; -import { - PopupField, - Field as NormalField, - EntryField, - PlainField, - createSettingFieldView, - SettingsPane, - createField, -} from '@ali/lowcode-editor-skeleton'; -import { createSetterContent } from '@ali/lowcode-editor-core'; -import { isPlainObject } from '@ali/lowcode-utils'; -import { isSetterConfig } from '@ali/lowcode-types'; -import context from './context'; -import { VE_HOOKS } from './base/const'; - -export class Placeholder extends Component { - render() { - console.info(this.props); - return 'rending placeholder here'; - } -} - -export class SettingField extends Component<{ - prop: any; - selected?: boolean; - forceDisplay?: string; - className?: string; - children?: ReactNode; - compact?: boolean; - key?: string; - addonProps?: object; -}> { - constructor(props: any) { - super(props); - - console.info(props); - } - - render() { - const { prop, selected, addonProps } = this.props; - const display = this.props.forceDisplay || prop.getDisplay(); - - if (display === 'none') { - return null; - } - - // 标准的属性,即每一个 Field 在 VE 下都拥有的属性 - const standardProps = { - className: this.props.className, - compact: this.props.compact, - - isSupportMultiSetter: this.supportMultiSetter(), - isSupportVariable: prop.isSupportVariable(), - isUseVariable: prop.isUseVariable(), - prop, - setUseVariable: () => prop.setUseVariable(!prop.isUseVariable()), - tip: prop.getTip(), - title: prop.getTitle(), - }; - - // 部分 Field 所需要的额外 fieldProps - const extraProps = {}; - const ctx = context; - const plugin = ctx.getPlugin(VE_HOOKS.VE_SETTING_FIELD_PROVIDER); - let Field; - if (typeof plugin === 'function') { - Field = plugin(display, FIELD_TYPE_MAP, prop); - } - if (!Field) { - Field = FIELD_TYPE_MAP[display] || PlainField; - } - createField() - this._prepareProps(display, extraProps); - - if (display === 'entry') { - return <Field {...{ ...standardProps, ...extraProps }} />; - } - - let setter; - const props: any = { - prop, - selected, - }; - const fieldProps = { ...standardProps, ...extraProps }; - - if (prop.isUseVariable() && !this.variableSetter.isPopup) { - props.placeholder = '请输入表达式: ${var}'; - props.key = `${prop.getId()}-variable`; - setter = React.createElement(this.variableSetter, props); - return <Field {...fieldProps}>{setter}</Field>; - } - - // for composited prop - if (prop.getVisibleItems) { - setter = prop - .getVisibleItems() - .map((item: any) => <SettingField {...{ key: item.getId(), prop: item, selected }} />); - return <Field {...fieldProps}>{setter}</Field>; - } - - setter = createSetterContent(prop.getSetter(), { - ...addonProps, - ...props, - }); - - return <Field {...fieldProps}>{setter}</Field>; - } - - private supportMultiSetter() { - const { prop } = this.props; - const setter = prop && prop.getConfig && prop.getConfig('setter'); - return prop.isSupportVariable() || Array.isArray(setter); - } - - private _prepareProps(displayType: string, extraProps: IExtraProps): void { - const { prop } = this.props; - extraProps.propName = prop.isGroup() ? '组合属性,无属性名称' : prop.getName(); - switch (displayType) { - case 'title': - break; - case 'block': - assign(extraProps, { isGroup: prop.isGroup() }); - break; - case 'accordion': - assign(extraProps, { - headDIY: true, - isExpand: prop.isExpand(), - isGroup: prop.isGroup(), - onExpandChange: () => prop.onExpandChange(() => this.forceUpdate()), - toggleExpand: () => { - prop.toggleExpand(); - }, - }); - break; - case 'entry': - assign(extraProps, { stageName: prop.getName() }); - break; - default: - break; - } - } -} - -const Field = { - SettingField: Placeholder, - Stage: Placeholder, - PopupField: Placeholder, - EntryField: Placeholder, - AccordionField: Placeholder, - BlockField: Placeholder, - InlineField: Placeholder, -}; - -export default Field; diff --git a/packages/vision-preset/src/fields/field.tsx b/packages/vision-preset/src/fields/field.tsx new file mode 100644 index 000000000..3fc13ea7e --- /dev/null +++ b/packages/vision-preset/src/fields/field.tsx @@ -0,0 +1,150 @@ +import classnames from 'classnames'; +import * as React from 'react'; +import { Component } from 'react'; +import InlineTip from './inlinetip'; +import { isPlainObject } from '@ali/lowcode-utils'; + +interface IHelpTip { + url?: string; + content?: string; +} + +function splitWord(title: string): JSX.Element[] { + return (title || '').split('').map((w, i) => <b key={`word${i}`} className='engine-word'>{w}</b>); +} + +function getFieldTitle(title: string, tip: IHelpTip, compact?: boolean, propName?: string): JSX.Element { + const className = classnames('engine-field-title', { 've-compact': compact }); + let titleContent = null; + + if (!compact && typeof title === 'string') { + titleContent = splitWord(title); + } + + let tipUrl = null; + let tipContent = null; + + tipContent = ( + <div> + <div>属性:{propName}</div> + </div> + ); + + if (isPlainObject(tip)) { + tipUrl = tip.url; + tipContent = ( + <div> + <div>属性:{propName}</div> + <div>说明:{tip.content}</div> + </div> + ); + } else if (tip) { + tipContent = ( + <div> + <div>属性:{propName}</div> + <div>说明:{tip}</div> + </div> + ); + } + return ( + <a + className={className} + target='_blank' + rel='noopener noreferrer' + href={tipUrl!} + > + {titleContent || (typeof title === 'object' ? '' : title)} + <InlineTip position='top'>{tipContent}</InlineTip> + </a> + ); +} + +export interface IVEFieldProps { + prop: any; + children: JSX.Element | string; + title?: string; + tip?: any; + propName?: string; + className?: string; + compact?: boolean; + stageName?: string; + /** + * render the top-header by jsx + */ + headDIY?: boolean; + + isSupportVariable?: boolean; + isSupportMultiSetter?: boolean; + isUseVariable?: boolean; + + isGroup?: boolean; + isExpand?: boolean; + + toggleExpand?: () => any; + onExpandChange?: (fn: () => any) => any; +} + +interface IVEFieldState { + hasError?: boolean; +} + +export default class VEField extends Component<IVEFieldProps, IVEFieldState> { + public static displayName = 'VEField'; + + public readonly props: IVEFieldProps; + public classNames: string[] = []; + + public state: IVEFieldState = { + hasError: false, + }; + + public componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error(error); + console.warn(info.componentStack); + } + + public renderHead(): JSX.Element | JSX.Element[] | null { + const { title, tip, compact, propName } = this.props; + return getFieldTitle(title!, tip, compact, propName); + } + + public renderBody(): JSX.Element | string { + return this.props.children; + } + + public renderFoot(): any { + return null; + } + + public render(): JSX.Element { + const { stageName, headDIY } = this.props; + const classNameList = classnames(...this.classNames, this.props.className); + const fieldProps: any = {}; + + if (stageName) { + // 为 stage 切换奠定基础 + fieldProps['data-stage-target'] = this.props.stageName; + } + + if (this.state.hasError) { + return ( + <div>Field render error, please open console to find out.</div> + ); + } + + const headContent = headDIY ? this.renderHead() + : <div className='engine-field-head'>{this.renderHead()}</div>; + + return ( + <div className={classNameList} { ...fieldProps }> + {headContent} + <div className='engine-field-body'> + {this.renderBody()} + </div> + <div className='engine-field-foot'> + {this.renderFoot()} + </div> + </div> + ); + } +} diff --git a/packages/vision-preset/src/fields/fields.less b/packages/vision-preset/src/fields/fields.less new file mode 100644 index 000000000..c02417425 --- /dev/null +++ b/packages/vision-preset/src/fields/fields.less @@ -0,0 +1,272 @@ +@import '~@ali/ve-less-variables/index.less'; + +.engine-setting-field { + white-space: nowrap; + position: relative; + + &:after, &:before { + content: " "; + display: table; + } + &:after { + clear: both; + } + + .engine-field-title { + font-size: 12px; + font-family: @font-family; + line-height: 1em; + user-select: none; + color: var(--color-text, @dark-alpha-3); + width: fit-content; + white-space: initial; + word-break: break-word; + &::first-letter { + text-transform: capitalize; + } + + .engine-word { + flex: 1; + text-align: center; + font-weight: normal; + &:first-child { + text-align: left; + } + &:last-of-type { + text-align: right; + } + &:only-of-type { + text-align: center; + } + overflow: hidden; + } + } + + a.engine-field-title { + border-bottom: 1px dashed var(--color-line-normal, @normal-alpha-7); + text-decoration: none; + padding-bottom: 2px; + &:hover { + cursor: help; + } + } + + .engine-field-variable-wrapper { + margin-left: 5px; + } + + .engine-field-variable { + cursor: pointer; + opacity: 0.6; + &.engine-active { + opacity: 1; + color: var(--color-brand, @brand-color-1); + } + } + + .engine-field-head { + padding-left: 10px; + height: 32px; + background: var(--color-block-background-shallow, @normal-alpha-8); + display: flex; + align-items: center; + font-weight: 500; + border-top: 1px solid var(--color-line-normal, @normal-alpha-7); + border-bottom: 1px solid var(--color-line-normal, @normal-alpha-7); + color: var(--color-title, @dark-alpha-2); + >.engine-icontip { + margin-left: 2px; + } + } + + .engine-field-body { + min-height: 20px; + margin: 6px 0; + + &:after, &:before { + content: " "; + display: table; + } + &:after { + clear: both; + } + + .engine-field-head { + height: 28px; + border: none; + font-weight: 400; + } + } + + &.engine-plain-field { + >.engine-field-variable { + position: absolute; + right: 5px; + top: 8px; + } + &:hover { + >.engine-field-variable { + opacity: 1; + } + } + } + + &.engine-entry-field { + cursor: pointer; + display: flex; + align-items: center; + height: 32px; + padding-left: 10px; + font-weight: 500; + border-top: 1px solid var(--color-line-normal, @normal-alpha-7); + border-bottom: 1px solid var(--color-line-normal, @normal-alpha-7); + background: var(--color-block-background-shallow, @normal-alpha-8); + margin-bottom: 6px; + + >.engine-field-title { + letter-spacing: 1px; + } + + >.engine-icontip { + margin-left: 2px; + } + + >.engine-field-arrow { + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%) rotate(-90deg); + opacity: 0.4; + } + &:hover { + >.engine-field-arrow { + opacity: 1; + } + } + } + + &.engine-popup-field { + cursor: pointer; + display: flex; + align-items: center; + height: 32px; + padding-left: 10px; + background: var(--color-block-background-shallow, @normal-alpha-8); + margin-bottom: 1px; + + >.engine-field-title { + letter-spacing: 1px; + } + + >.engine-icontip { + margin-left: 2px; + } + + >.engine-field-icon { + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%); + opacity: 0.6; + } + &:hover { + >.engine-field-icon { + opacity: 1; + } + } + } + + &.engine-block-field { + >.engine-field-head{ + > .engine-field-title { + letter-spacing: 1px; + } + >.engine-field-variable { + margin-left: 2px; + } + } + >.engine-field-body { + margin: 6px; + } + } + + &.engine-inline-field { + display: flex; + align-items: center; + margin: 10px; + >.engine-field-head { + display: inline-flex; + background: none; + padding: 0; + border: none; + + >.engine-field-title { + display: inline-flex; + width: 50px; + margin-right: 5px; + } + } + >.engine-field-body { + width: 100%; + display: inline-flex; + align-items: flex-start; + padding: 0; + margin: 0; + flex: 1; + position: relative; + } + >.engine-field-variable { + margin-left: 2px; + } + &:hover { + >.engine-field-variable { + opacity: 1; + } + } + } + + &.engine-accordion-field { + >.engine-field-head { + position: relative; + cursor: pointer; + >.engine-field-title { + letter-spacing: 1px; + } + >.engine-field-arrow { + transform: rotate(180deg); + position: absolute; + right: 7px; + top: 7px; + transition: transform 0.1s ease; + opacity: 0.6; + } + >.engine-field-variable { + margin-left: 2px; + } + } + &.engine-collapsed { + >.engine-field-head { + margin-bottom: 6px; + } + >.engine-field-head > .engine-field-arrow { + transform: rotate(0); + } + >.engine-field-body { + display: none; + } + } + >.engine-field-body { + margin: 6px; + } + } +} + +.engine-block-field,.engine-accordion-field,.engine-entry-field { + .engine-input-control { + margin: 10px; + } +} + +.engine-field-tip-icon { + margin-left: 2px; +} diff --git a/packages/vision-preset/src/fields/fields.tsx b/packages/vision-preset/src/fields/fields.tsx new file mode 100644 index 000000000..e106fe7f7 --- /dev/null +++ b/packages/vision-preset/src/fields/fields.tsx @@ -0,0 +1,376 @@ +import Icons from '@ali/ve-icons'; +import classNames from 'classnames'; +import { Component } from 'react'; +import { testType } from '@ali/ve-utils'; +import VEField, { IVEFieldProps } from './field'; +import { SettingField } from './settingField'; +import VariableSwitcher from './variableSwitcher'; +import popups from '@ali/ve-popups'; + +import './fields.less'; + +interface IHelpTip { + url?: string; + content?: string | JSX.Element; +} + +function renderTip(tip: IHelpTip, prop?: { propName?: string }) { + const propName = prop && prop.propName; + if (!tip) { + return ( + <Icons.Tip position="top" key="icon" className="engine-field-tip-icon"> + <div> + <div>{propName}</div> + </div> + </Icons.Tip> + ); + } + if (testType(tip) === 'object') { + return ( + <Icons.Tip position="top" url={tip.url} key="icon-tip" className="engine-field-tip-icon"> + <div> + <div>属性:{propName}</div> + <div>说明:{tip.content}</div> + </div> + </Icons.Tip> + ); + } + return ( + <Icons.Tip position="top" key="icon" className="engine-field-tip-icon"> + <div> + <div>属性:{propName}</div> + <div>说明:{tip}</div> + </div> + </Icons.Tip> + ); +} + +export class PlainField extends VEField { + public static defaultProps = { + headDIY: true, + }; + + public static displayName: string = 'PlainField'; + + public renderHead(): null { + return null; + } +} + +export class InlineField extends VEField { + public static displayName = 'InlineField'; + constructor(props: any) { + super(props); + this.classNames = ['engine-setting-field', 'engine-inline-field']; + } + + public renderFoot() { + return ( + <div className="engine-field-variable-wrapper"> + <VariableSwitcher {...this.props} /> + </div> + ); + } +} + +export class BlockField extends VEField { + public static displayName = 'BlockField'; + + constructor(props: IVEFieldProps) { + super(props); + this.classNames = ['engine-setting-field', 'engine-block-field', props.isGroup ? 'engine-group-field' : '']; + } + + public renderHead() { + const { title, tip, propName } = this.props; + return [ + <span className="engine-field-title" key={title}> + {title} + </span>, + renderTip(tip, { propName }), + <VariableSwitcher {...this.props} />, + ]; + } +} + +export class AccordionField extends VEField { + public readonly props: IVEFieldProps; + + private willDetach?: () => any; + + constructor(props: IVEFieldProps) { + super(props); + this._generateClassNames(props); + if (this.props.onExpandChange) { + this.willDetach = this.props.onExpandChange(() => this.forceUpdate()); + } + } + + public componentWillReceiveProps(nextProps: IVEFieldProps) { + this.classNames = this._generateClassNames(nextProps); + } + + public componentWillUnmount() { + if (this.willDetach) { + this.willDetach(); + } + } + + public renderHead() { + const { title, tip, toggleExpand, propName } = this.props; + return ( + <div className="engine-field-head" onClick={() => toggleExpand && toggleExpand()}> + <Icons name="arrow" className="engine-field-arrow" size="12px" /> + <span className="engine-field-title">{title}</span> + {renderTip(tip, { propName })} + {<VariableSwitcher {...this.props} />} + </div> + ); + } + + private _generateClassNames(props: IVEFieldProps) { + this.classNames = [ + 'engine-setting-field', + 'engine-accordion-field', + props.isGroup ? 'engine-group-field' : '', + !props.isExpand ? 'engine-collapsed' : '', + ]; + return this.classNames; + } +} + +export class EntryField extends VEField { + constructor(props: any) { + super(props); + this.classNames = ['engine-setting-field', 'engine-entry-field']; + } + + public render() { + const { propName, stageName, tip, title } = this.props; + const classNameList = classNames(...this.classNames, this.props.className); + const fieldProps: any = {}; + + if (stageName) { + // 为 stage 切换奠定基础 + fieldProps['data-stage-target'] = this.props.stageName; + } + + const innerElements = [ + <span className="engine-field-title" key="field-title"> + {title} + </span>, + renderTip(tip, { propName }), + <Icons name="arrow" className="engine-field-arrow" size="12px" key="engine-field-arrow-icon" />, + ]; + + return ( + <div className={classNameList} {...fieldProps}> + {innerElements} + </div> + ); + } +} + +export class PopupField extends VEField { + constructor(props: any) { + super(props); + this.classNames = ['engine-setting-field', 'engine-popup-field']; + } + + public renderBody() { + return ''; + } + + public render() { + const { propName, stageName, tip, title } = this.props; + const classNameList = classNames(...this.classNames, this.props.className); + const fieldProps: any = {}; + + if (stageName) { + // 为 stage 切换奠定基础 + fieldProps['data-stage-target'] = this.props.stageName; + } + + return ( + <div + className={classNameList} + onClick={(e) => + popups.popup({ + cancelOnBlur: true, + content: this.props.children, + position: 'left bottom', + showClose: true, + sizeFixed: true, + target: e.currentTarget, + }) + } + > + <span className="engine-field-title">{title}</span> + {renderTip(tip, { propName })} + <VariableSwitcher {...this.props} /> + <Icons name="popup" className="engine-field-icon" size="medium" /> + </div> + ); + } +} + +export class CaptionField extends VEField { + constructor(props: IVEFieldProps) { + super(props); + this.classNames = ['engine-setting-field', 'engine-caption-field']; + } + + public renderHead() { + const { title, tip, propName } = this.props; + return ( + <div> + <span className="engine-field-title">{title}</span> + {renderTip(tip, { propName })} + </div> + ); + } +} + +export class Stage extends Component { + public readonly props: { + key: any; + stage: any; + current?: boolean; + direction?: any; + }; + + public stage: any; + public additionClassName: string; + public shell: Element | null = null; + private willDetach: () => any; + + public componentWillMount() { + this.stage = this.props.stage; + if (this.stage.onCurrentTabChange) { + this.willDetach = this.stage.onCurrentTabChange(() => this.forceUpdate()); + } + } + + public componentDidMount() { + this.doSkate(); + } + + public componentWillReceiveProps(props: any) { + if (props.stage !== this.stage) { + this.stage = props.stage; + if (this.willDetach) { + this.willDetach(); + } + if (this.stage.onCurrentTabChange) { + this.willDetach = this.stage.onCurrentTabChange(() => this.forceUpdate()); + } + } + } + + public componentDidUpdate() { + this.doSkate(); + } + + public componentWillUnmount() { + if (this.willDetach) { + this.willDetach(); + } + } + + public doSkate() { + if (this.additionClassName) { + setTimeout(() => { + const elem = this.shell; + if (elem && elem.classList) { + if (this.props.current) { + elem.classList.remove(this.additionClassName); + } else { + elem.classList.add(this.additionClassName); + } + this.additionClassName = ''; + } + }, 10); + } + } + + public render() { + const stage = this.stage; + let content = null; + let tabs = null; + + let className = 'engine-settings-stage'; + + if (stage.getTabs) { + const selected = stage.getNode(); + // stat for cache + stage.stat(); + const currentTab = stage.getCurrentTab(); + + if (stage.hasTabs()) { + className += ' engine-has-tabs'; + tabs = ( + <div className="engine-settings-tabs"> + {stage.getTabs().map((tab: any) => ( + <div + key={tab.getId()} + className={`engine-settings-tab${tab === currentTab ? ' engine-active' : ''}`} + onClick={() => stage.setCurrentTab(tab)} + > + {tab.getTitle()} + {renderTip(tab.getTip())} + </div> + ))} + </div> + ); + } + + if (currentTab) { + if (currentTab.getVisibleItems) { + content = currentTab + .getVisibleItems() + .map((item: any) => <SettingField key={item.getId()} selected={selected} prop={item} />); + } else if (currentTab.getSetter) { + content = ( + <SettingField key={currentTab.getId()} selected={selected} prop={currentTab} forceDisplay="plain" /> + ); + } + } + } else { + content = stage.getContent(); + } + + if (this.props.current) { + if (this.props.direction) { + this.additionClassName = `engine-stagein-${this.props.direction}`; + className += ` ${this.additionClassName}`; + } + } else if (this.props.direction) { + this.additionClassName = `engine-stageout-${this.props.direction}`; + } + + let stageBacker = null; + if (stage.hasBack()) { + className += ' engine-has-backer'; + stageBacker = ( + <div className="engine-settings-stagebacker" data-stage-target="stageback"> + <Icons name="arrow" className="engine-field-arrow" size="12px" /> + <span className="engine-field-title">{stage.getTitle()}</span> + {renderTip(stage.getTip())} + </div> + ); + } + + return ( + <div + ref={(ref) => { + this.shell = ref; + }} + className={className} + > + {stageBacker} + {tabs} + <div className="engine-stage-content">{content}</div> + </div> + ); + } +} diff --git a/packages/vision-preset/src/fields/index.ts b/packages/vision-preset/src/fields/index.ts new file mode 100644 index 000000000..682cba49d --- /dev/null +++ b/packages/vision-preset/src/fields/index.ts @@ -0,0 +1,2 @@ +export * from './settingField'; +export * from './fields'; diff --git a/packages/vision-preset/src/fields/inlinetip.tsx b/packages/vision-preset/src/fields/inlinetip.tsx new file mode 100644 index 000000000..f3344b1c3 --- /dev/null +++ b/packages/vision-preset/src/fields/inlinetip.tsx @@ -0,0 +1,30 @@ +import { Component } from 'react'; + +export interface InlineTipProps { + position: string; + theme?: 'green' | 'black'; + children: React.ReactNode; +} + +export default class InlineTip extends Component<InlineTipProps> { + public static displayName = 'InlineTip'; + + public static defaultProps = { + position: 'auto', + theme: 'black', + }; + + public render(): React.ReactNode { + const { position, theme, children } = this.props; + return ( + <div + style={{ display: 'none' }} + data-role='tip' + data-position={position} + data-theme={theme} + > + {children} + </div> + ); + } +} diff --git a/packages/vision-preset/src/fields/settingField.tsx b/packages/vision-preset/src/fields/settingField.tsx new file mode 100644 index 000000000..4edd0c52d --- /dev/null +++ b/packages/vision-preset/src/fields/settingField.tsx @@ -0,0 +1,186 @@ +import VariableSetter from './variableSetter'; +import context from '../context'; +import { VE_HOOKS } from '../base/const'; +import { + AccordionField, + BlockField, + EntryField, + InlineField, + PlainField, + PopupField +} from "./fields"; + +import { ComponentClass, Component, isValidElement, createElement } from 'react'; +import { createSetterContent, getSetter } from '@ali/lowcode-editor-core'; + +function isReactClass(obj: any): obj is ComponentClass<any> { + return ( + obj && + obj.prototype && + (obj.prototype.isReactComponent || obj.prototype instanceof Component) + ); +} + +interface IExtraProps { + stageName?: string; + isGroup?: boolean; + isExpand?: boolean; + propName?: string; + toggleExpand?: () => any; + onExpandChange?: () => any; +} + +const FIELD_TYPE_MAP: any = { + accordion: AccordionField, + block: BlockField, + entry: EntryField, + inline: InlineField, + plain: PlainField, + popup: PopupField, + tab: AccordionField +}; + +export class SettingField extends Component { + public readonly props: { + prop: any; + selected?: boolean; + forceDisplay?: string; + className?: string; + children?: JSX.Element | string; + compact?: boolean; + key?: string; + addonProps?: object; + }; + + /** + * VariableSetter placeholder + */ + public variableSetter: any; + + constructor(props: any) { + super(props); + + this.variableSetter = getSetter('VariableSetter')?.component || VariableSetter; + } + + public render() { + const { prop, selected, addonProps } = this.props; + const display = this.props.forceDisplay || prop.getDisplay(); + + if (display === "none") { + return null; + } + + // 标准的属性,即每一个 Field 在 VE 下都拥有的属性 + const standardProps = { + className: this.props.className, + compact: this.props.compact, + + isSupportMultiSetter: this.supportMultiSetter(), + isSupportVariable: prop.isSupportVariable(), + isUseVariable: prop.isUseVariable(), + prop, + setUseVariable: () => prop.setUseVariable(!prop.isUseVariable()), + tip: prop.getTip(), + title: prop.getTitle() + }; + + // 部分 Field 所需要的额外 fieldProps + const extraProps = {}; + const ctx = context; + const plugin = ctx.getPlugin(VE_HOOKS.VE_SETTING_FIELD_PROVIDER); + let Field; + if (typeof plugin === "function") { + Field = plugin(display, FIELD_TYPE_MAP, prop); + } + if (!Field) { + Field = FIELD_TYPE_MAP[display] || PlainField; + } + this._prepareProps(display, extraProps); + + if (display === "entry") { + return <Field {...{ ...standardProps, ...extraProps }} />; + } + + let setter; + const props: any = { + prop, + selected, + }; + const fieldProps = { ...standardProps, ...extraProps }; + + if (prop.isUseVariable() && !this.variableSetter.isPopup) { + props.placeholder = "请输入表达式: ${var}"; + props.key = `${prop.getId()}-variable`; + setter = createElement(this.variableSetter, props); + return <Field {...fieldProps}>{setter}</Field>; + } + + // for composited prop + if (prop.getVisibleItems) { + setter = prop + .getVisibleItems() + .map((item: any) => ( + <SettingField {...{ key: item.getId(), prop: item, selected }} /> + )); + return <Field {...fieldProps}>{setter}</Field>; + } + + setter = prop.getSetter(); + if ( + typeof setter === "object" && + "componentName" in setter && + !(isValidElement(setter) || isReactClass(setter)) + ) { + const { componentName: setterType, props: setterProps } = setter as any; + setter = createSetterContent(setterType, { + ...addonProps, + ...setterProps, + ...props + }); + } else { + setter = createSetterContent(setter, { + ...addonProps, + ...props + }); + } + + return <Field {...fieldProps}>{setter}</Field>; + } + + private supportMultiSetter() { + const { prop } = this.props; + const setter = prop && prop.getConfig && prop.getConfig("setter"); + return prop.isSupportVariable() || Array.isArray(setter); + } + + private _prepareProps(displayType: string, extraProps: IExtraProps): void { + const { prop } = this.props; + extraProps.propName = prop.isGroup() + ? "组合属性,无属性名称" + : prop.getName(); + switch (displayType) { + case "title": + break; + case "block": + Object.assign(extraProps, { isGroup: prop.isGroup() }); + break; + case "accordion": + Object.assign(extraProps, { + headDIY: true, + isExpand: prop.isExpand(), + isGroup: prop.isGroup(), + onExpandChange: () => prop.onExpandChange(() => this.forceUpdate()), + toggleExpand: () => { + prop.toggleExpand(); + } + }); + break; + case "entry": + Object.assign(extraProps, { stageName: prop.getName() }); + break; + default: + break; + } + } +} diff --git a/packages/vision-preset/src/fields/variableSetter.less b/packages/vision-preset/src/fields/variableSetter.less new file mode 100644 index 000000000..6ea5653d3 --- /dev/null +++ b/packages/vision-preset/src/fields/variableSetter.less @@ -0,0 +1,43 @@ +@import '~@ali/ve-less-variables/index.less'; + +.engine-input-control { + box-sizing: border-box; + font-size: 12px; + font-family: Consolas, "Courier New", Courier, FreeMono, monospace; + color: var(--color-text, @dark-alpha-3); + background: var(--color-field-background, @white-alpha-1); + border: 1px solid var(--color-field-border, @normal-alpha-5); + flex: 1; + border-radius: @global-border-radius; + max-height: 200px; + + &:hover { + border-color: var(--color-field-border-hover, @normal-alpha-4); + } + + &.engine-focused { + border-color: var(--color-field-border-active, @normal-alpha-3); + } + + textarea { + resize: none; + } + + >.engine-input { + box-sizing: border-box; + padding: 6px; + display: block; + font-size: 12px; + line-height: 16px; + color: var(--color-text, @dark-alpha-3); + width: 100%; + border: 0; + margin: 0; + background: transparent; + outline: none; + + &::-webkit-input-placeholder { + color: var(--color-field-placeholder, @normal-alpha-5); + } + } +} diff --git a/packages/vision-preset/src/fields/variableSetter.tsx b/packages/vision-preset/src/fields/variableSetter.tsx new file mode 100644 index 000000000..51643bd16 --- /dev/null +++ b/packages/vision-preset/src/fields/variableSetter.tsx @@ -0,0 +1,85 @@ +import './variableSetter.less'; +import { Component } from 'react'; + +class Input extends Component { + public props: { + value: string; + placeholder: string; + onChange: (val: any) => any; + }; + + public state: { focused: boolean }; + + constructor(props: object) { + super(props); + this.state = { + focused: false, + }; + } + + public componentDidMount() { + this.adjustTextAreaHeight(); + } + + private domRef: HTMLTextAreaElement | null = null; + public adjustTextAreaHeight() { + if (!this.domRef) { + return; + } + this.domRef.style.height = '1px'; + const calculatedHeight = this.domRef.scrollHeight; + this.domRef.style.height = calculatedHeight >= 200 ? '200px' : calculatedHeight + 'px'; + } + + public render() { + const { value, placeholder, onChange } = this.props; + return ( + <div + className={`engine-variable-setter-input engine-input-control${this.state.focused ? ' engine-focused' : ''}`} + > + <textarea + ref={(r) => { + this.domRef = r; + }} + className="engine-input" + value={value || ''} + placeholder={placeholder || ''} + onChange={(e) => { + onChange(e.target.value || ''); + }} + onBlur={() => this.setState({ focused: false })} + onFocus={() => this.setState({ focused: true })} + onKeyUp={this.adjustTextAreaHeight.bind(this)} + ></textarea> + </div> + ); + } +} + +export default class VariableSetter extends Component<{ + prop: any; + placeholder: string; +}> { + public willDetach: () => any; + + public componentWillMount() { + this.willDetach = this.props.prop.onValueChange(() => this.forceUpdate()); + } + + public componentWillUnmount() { + if (this.willDetach) { + this.willDetach(); + } + } + + public render() { + const prop = this.props.prop; + return ( + <Input + value={prop.getVariableValue()} + placeholder={this.props.placeholder} + onChange={(val: string) => prop.setVariableValue(val)} + /> + ); + } +} diff --git a/packages/vision-preset/src/fields/variableSwitcher.less b/packages/vision-preset/src/fields/variableSwitcher.less new file mode 100644 index 000000000..0991b0ed6 --- /dev/null +++ b/packages/vision-preset/src/fields/variableSwitcher.less @@ -0,0 +1,20 @@ +@import '~@ali/ve-less-variables/index.less'; + +.engine-field-variable-switcher { + cursor: pointer; + opacity: 0.6; + margin-left: 2px; + + &.engine-active { + opacity: 1; + background: var(--color-brand, @brand-color-1); + color: #fff !important; + border-radius: 3px; + margin-left: 4px; + + svg { + height: 22px !important; + width: 22px !important; + } + } +} diff --git a/packages/vision-preset/src/fields/variableSwitcher.tsx b/packages/vision-preset/src/fields/variableSwitcher.tsx new file mode 100644 index 000000000..146ad7502 --- /dev/null +++ b/packages/vision-preset/src/fields/variableSwitcher.tsx @@ -0,0 +1,57 @@ +import VariableSetter from './variableSetter'; +import Icons from '@ali/ve-icons'; +import { IVEFieldProps } from './field'; +import './variableSwitcher.less'; +import { Component } from 'react'; +import { getSetter } from '@ali/lowcode-editor-core'; + +interface IState { + visible: boolean; +} + +export default class VariableSwitcher extends Component<IVEFieldProps, IState> { + private ref: HTMLElement | null = null; + private VariableSetter: any; + + constructor(props: IVEFieldProps) { + super(props); + + this.VariableSetter = getSetter('VariableSetter')?.component || VariableSetter; + + this.state = { + visible: false, + }; + } + + public render() { + const { isUseVariable, prop } = this.props; + const { visible } = this.state; + const isSupportVariable = prop.isSupportVariable(); + const tip = !isUseVariable ? '绑定变量' : prop.getVariableValue(); + if (!isSupportVariable) { + return null; + } + return ( + <div> + <Icons.Tip + name='var' + size='24px' + position='bottom center' + className={`engine-field-variable-switcher ${isUseVariable ? 'engine-active' : ''}`} + data-tip={tip} + onClick={(e: Event) => { + e.stopPropagation(); + if (this.VariableSetter.isPopup) { + this.VariableSetter.show({ + prop, + }); + } else { + prop.setUseVariable(!isUseVariable); + } + }}> + 绑定变量 + </Icons.Tip> + </div> + ); + } +} diff --git a/packages/vision-preset/src/index.ts b/packages/vision-preset/src/index.ts index 31185f2d2..2c4db40e9 100644 --- a/packages/vision-preset/src/index.ts +++ b/packages/vision-preset/src/index.ts @@ -17,7 +17,7 @@ import Trunk from './bundle/trunk'; import Prototype from './bundle/prototype'; import Bundle from './bundle/bundle'; import Pages from './pages'; -import Field from './field'; +import * as Field from './fields'; import Prop from './prop'; import Env from './env'; import DragEngine from './drag-engine'; @@ -26,6 +26,11 @@ import { designer, editor } from './editor'; import './vision.less'; function init(container?: Element) { + //TODO: dirty fix + // 之前的组件库依赖了这个样式,临时fix一下。 + // 取决于预览模式是否保留。 + document.documentElement.classList.add('engine-design-mode'); + if (!container) { container = document.createElement('div'); document.body.appendChild(container); @@ -35,6 +40,7 @@ function init(container?: Element) { render( createElement(Workbench, { skeleton, + className: 'engine-main', }), container, ); diff --git a/packages/vision-preset/src/panes.ts b/packages/vision-preset/src/panes.ts index 750307496..792628d51 100644 --- a/packages/vision-preset/src/panes.ts +++ b/packages/vision-preset/src/panes.ts @@ -1,6 +1,6 @@ import { skeleton, editor } from './editor'; import { ReactElement } from 'react'; -import { IWidgetBaseConfig } from '@ali/lowcode-editor-skeleton'; +import { IWidgetBaseConfig, Skeleton } from '@ali/lowcode-editor-skeleton'; import { uniqueId } from '@ali/lowcode-utils'; export interface IContentItemConfig { @@ -85,6 +85,7 @@ function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: strin } if (!isAction) { newConfig.panelProps = { + title, hideTitleBar, help: tip, width, @@ -156,6 +157,10 @@ const dockPane = Object.assign(skeleton.leftArea, { * compatible *VE.dockPane.activeDock* */ activeDock(item: any) { + if (!item) { + skeleton.leftFloatArea?.current?.hide(); + return; + } const name = item.name || item; skeleton.getPanel(name)?.active(); }, diff --git a/packages/vision-preset/src/vision.less b/packages/vision-preset/src/vision.less index 16d9ddc3c..492464688 100644 --- a/packages/vision-preset/src/vision.less +++ b/packages/vision-preset/src/vision.less @@ -46,6 +46,23 @@ html.engine-blur #engine { -webkit-filter: blur(4px); } +.engine-main { + width: 100%; + height: 100%; + position: relative; + + .ve-icon-button { + > .ve-icon-contents { + color: var(--color-text, rgba(51,51,51,.6)); + } + &:hover, &.active { + > .ve-icon-contents { + color: var(--color-text-light, rgba(51,51,51,.8)); + } + } + } +} + .engine-empty { background: #f2f3f5; color: #a7b1bd;