From b486a8419cf1f02ad757b2cb279f111e0f7b2fde Mon Sep 17 00:00:00 2001 From: kangwei Date: Mon, 27 Apr 2020 00:07:43 +0800 Subject: [PATCH] optimize structure --- .../bem-tools/border-hovering.tsx | 3 +- .../bem-tools/border-selecting.tsx | 26 +- .../src/builtin-simulator/bem-tools/index.tsx | 2 +- .../builtin-simulator/bem-tools/insertion.tsx | 4 +- .../src/builtin-simulator/create-simulator.ts | 4 +- .../src/builtin-simulator/host-view.tsx | 2 +- .../designer/src/builtin-simulator/host.ts | 11 +- .../builtin-simulator/resource-consumer.ts | 2 +- .../builtin-simulator/utils/parse-metadata.ts | 4 +- .../src/builtin-simulator/viewport.ts | 2 +- packages/designer/src/component-meta.ts | 51 ++- .../designer/src/designer/builtin-hotkey.ts | 7 +- .../designer/src/designer/designer-view.tsx | 1 - packages/designer/src/designer/designer.ts | 6 +- .../src/designer/drag-ghost/index.tsx | 2 +- packages/designer/src/designer/dragon.ts | 6 +- packages/designer/src/designer/hovering.ts | 2 +- .../designer/src/designer/offset-observer.ts | 4 +- packages/designer/src/designer/scroller.ts | 2 +- .../src/designer/setting/setting-entry.ts | 2 +- .../src/designer/setting/setting-field.ts | 3 +- .../designer/setting/setting-prop-entry.ts | 4 +- .../src/designer/setting/setting-top-entry.ts | 3 +- .../designer/src/document/document-model.ts | 29 +- .../designer/src/document/document-view.tsx | 2 +- packages/designer/src/document/history.ts | 3 +- .../src/document/node/exclusive-group.ts | 5 +- .../src/document/node/node-children.ts | 3 +- packages/designer/src/document/node/node.ts | 17 +- .../src/document/node/props/prop-stash.ts | 2 +- .../designer/src/document/node/props/prop.ts | 16 +- .../designer/src/document/node/props/props.ts | 6 +- packages/designer/src/document/selection.ts | 2 +- packages/designer/src/icons/clone.tsx | 2 +- packages/designer/src/icons/component.tsx | 2 +- packages/designer/src/icons/container.tsx | 2 +- packages/designer/src/icons/hidden.tsx | 2 +- packages/designer/src/icons/page.tsx | 2 +- packages/designer/src/icons/remove.tsx | 2 +- packages/designer/src/icons/setting.tsx | 2 +- packages/designer/src/locale/index.ts | 2 +- .../designer/src/project/project-view.tsx | 2 +- packages/designer/src/project/project.ts | 3 +- packages/designer/src/simulator.ts | 2 +- packages/editor-core/CHANGELOG.md | 47 --- packages/editor-core/README.md | 12 +- packages/editor-core/build.json | 6 +- .../{globals => editor-core}/cloud-build.json | 0 packages/editor-core/package.json | 51 ++- packages/editor-core/src/areaManager.ts | 75 ---- packages/editor-core/src/context.ts | 4 - .../{globals => editor-core}/src/di/index.ts | 3 +- .../src/di/ioc-context.ts | 0 .../{globals => editor-core}/src/di/setter.ts | 5 +- packages/editor-core/src/editor.ts | 151 +------ .../src/utils => editor-core/src}/hotkey.ts | 0 packages/editor-core/src/index.ts | 17 +- .../src/intl/ali-global-locale.ts | 3 +- .../src/intl/index.ts} | 133 +++---- packages/editor-core/src/pluginFactory.tsx | 84 ---- packages/editor-core/src/utils.ts | 273 ------------- .../editor-core/src/{ => utils}/app-preset.ts | 0 .../src/utils/get-public-path.ts | 0 packages/editor-core/src/utils/goldlog.ts | 11 + packages/editor-core/src/utils/index.ts | 4 + .../index.ts => editor-core/src/utils/obx.ts} | 0 .../editor-core/src/{ => utils}/request.ts | 14 +- .../src/widgets}/index.ts | 2 +- packages/editor-core/src/widgets/tip/index.ts | 4 + .../src/widgets}/tip/style.less | 0 .../src/widgets}/tip/tip-container.tsx | 8 +- .../src/widgets}/tip/tip-handler.ts | 98 ++--- .../src/widgets/tip/tip-item.tsx} | 9 +- packages/editor-core/src/widgets/tip/tip.tsx | 17 + .../src/widgets}/tip/utils.ts | 0 .../src/widgets}/title/index.tsx | 12 +- .../src/widgets}/title/title.less | 0 packages/editor-core/tsconfig.json | 4 +- packages/editor-skeleton/src/area.ts | 59 +++ .../src/components/LeftPlugin/index.scss | 59 --- .../src/components/LeftPlugin/index.tsx | 221 ----------- .../src/components/Panel/index.scss | 52 --- .../src/components/Panel/index.tsx | 60 --- .../src/components/TopIcon/index.scss | 77 ---- .../src/components/TopIcon/index.tsx | 63 --- .../src/components/TopPlugin/index.scss | 2 - .../src/components/TopPlugin/index.tsx | 219 ---------- .../src/components/array-setter/bugs.md | 5 + .../src/components/array-setter/index.tsx | 280 +++++++++++++ .../src/components/array-setter/sortable.less | 29 ++ .../src/components/array-setter/sortable.tsx | 220 ++++++++++ .../src/components/array-setter/style.less | 103 +++++ .../src/components/field/fields.tsx | 185 +++++++++ .../src/components/field/index.less | 154 +++++++ .../src/components/field/index.ts | 28 ++ .../src/components/mixed-setter/index.tsx | 262 ++++++++++++ .../src/components/mixed-setter/style.less | 30 ++ .../src/components/object-setter/index.tsx | 181 +++++++++ .../src/components/object-setter/style.less | 31 ++ .../src/components/popup/index.tsx | 150 +++++++ .../src/components/popup/style.less | 22 + .../src/components/settings/index.ts | 7 + .../src/components/settings/main.ts | 84 ++++ .../src/components/settings/package.json | 49 +++ .../src/components/settings/register.ts | 30 ++ .../src/components/settings/settings-pane.tsx | 152 +++++++ .../settings/settings-primary-view.tsx | 121 ++++++ .../src/components/settings/style.less | 124 ++++++ .../settings/transducers/addon-combine.ts | 255 ++++++++++++ .../settings/transducers/parse-props.ts | 232 +++++++++++ .../settings/transducers/register.ts | 9 + .../src/components/settings/utils.js | 41 ++ .../src/components/widget-views.tsx | 236 +++++++++++ .../editor-skeleton/src/config/skeleton.ts | 1 - packages/editor-skeleton/src/config/utils.ts | 1 - packages/editor-skeleton/src/dock.ts | 87 ++++ packages/editor-skeleton/src/global.scss | 33 -- .../editor-skeleton/src/icons/convert.tsx | 16 + packages/editor-skeleton/src/index.ts | 10 +- .../src/layouts/CenterArea/index.scss | 3 - .../src/layouts/CenterArea/index.tsx | 60 --- .../src/layouts/LeftArea/index.scss | 23 -- .../src/layouts/LeftArea/index.tsx | 7 - .../src/layouts/LeftArea/nav.tsx | 148 ------- .../src/layouts/LeftArea/panel.tsx | 88 ---- .../src/layouts/RightArea/index.scss | 39 -- .../src/layouts/RightArea/index.tsx | 214 ---------- .../src/layouts/TopArea/index.scss | 30 -- .../src/layouts/TopArea/index.tsx | 102 ----- .../src/layouts/bottom-area.tsx | 37 ++ .../editor-skeleton/src/layouts/left-area.tsx | 41 ++ .../src/layouts/left-fixed-pane.tsx | 50 +++ .../src/layouts/left-float-pane.tsx | 73 ++++ .../editor-skeleton/src/layouts/main-area.tsx | 21 + .../src/layouts/right-area.tsx | 35 ++ .../src/layouts}/theme.less | 0 .../editor-skeleton/src/layouts/toolbar.tsx | 49 +++ .../editor-skeleton/src/layouts/top-area.tsx | 44 ++ .../src/layouts}/workbench.less | 0 .../editor-skeleton/src/layouts/workbench.tsx | 40 ++ packages/editor-skeleton/src/locale/en-US.js | 1 - packages/editor-skeleton/src/locale/ja-JP.js | 1 - packages/editor-skeleton/src/locale/zh-CN.js | 1 - packages/editor-skeleton/src/locale/zh-TW.js | 1 - packages/editor-skeleton/src/panel.ts | 166 ++++++++ packages/editor-skeleton/src/skeleton.ts | 318 +++++++++++++++ packages/editor-skeleton/src/skeleton.tsx | 185 --------- packages/editor-skeleton/src/types.ts | 114 ++++++ .../src/widget}/dialog-dock.ts | 0 packages/editor-skeleton/src/widget/dock.ts | 87 ++++ .../editor-skeleton/src/widget/panel-dock.ts | 118 ++++++ packages/editor-skeleton/src/widget/panel.ts | 166 ++++++++ .../src/widget}/stage.ts | 4 +- .../src/widget}/utils.ts | 0 .../src/widget}/widget-container.ts | 0 packages/editor-skeleton/src/widget/widget.ts | 101 +++++ packages/globals/CHANGELOG.md | 78 ---- packages/globals/README.md | 3 - packages/globals/build.json | 9 - .../globals/src/components/tip/embed-tip.tsx | 17 - packages/globals/src/components/tip/index.ts | 4 - packages/globals/src/di/editor.ts | 33 -- packages/globals/src/di/transducer.ts | 31 -- packages/globals/src/index.ts | 6 - packages/plugin-components-pane/src/index.tsx | 2 +- .../plugin-event-bind-dialog/src/index.tsx | 2 +- .../src/helper/indent-track.ts | 6 +- .../src/icons/arrow-right.tsx | 2 +- .../plugin-outline-pane/src/icons/cond.tsx | 2 +- .../src/icons/eye-close.tsx | 2 +- .../plugin-outline-pane/src/icons/eye.tsx | 2 +- .../plugin-outline-pane/src/icons/lock.tsx | 2 +- .../plugin-outline-pane/src/icons/loop.tsx | 2 +- .../plugin-outline-pane/src/icons/outline.tsx | 2 +- .../plugin-outline-pane/src/icons/slot.tsx | 2 +- .../plugin-outline-pane/src/icons/unlock.tsx | 2 +- .../plugin-outline-pane/src/locale/index.ts | 2 +- packages/plugin-outline-pane/src/main.ts | 6 +- packages/plugin-outline-pane/src/tree-node.ts | 5 +- .../plugin-outline-pane/src/views/pane.tsx | 2 +- .../src/views/tree-branches.tsx | 2 +- .../src/views/tree-node.tsx | 2 +- .../src/views/tree-title.tsx | 13 +- .../plugin-outline-pane/src/views/tree.tsx | 5 +- packages/plugin-sample-logo/src/index.tsx | 2 +- packages/plugin-sample-preview/src/index.tsx | 2 +- packages/plugin-source-editor/src/index.tsx | 2 +- .../plugin-variable-bind-dialog/src/index.tsx | 10 +- packages/plugin-zh-en/src/index.tsx | 6 +- .../src/renderer-view.tsx | 2 +- .../react-simulator-renderer/src/renderer.ts | 10 +- .../src/utils/get-client-rects.ts | 2 +- .../src/utils/loader.ts | 2 +- .../src/utils/react-find-dom-nodes.ts | 2 +- packages/setters/package.json | 2 +- packages/setters/src/index.tsx | 2 - packages/setters/src/mixin-setter/index.tsx | 2 +- packages/setters/src/style-setter/index.tsx | 2 +- packages/types/README.md | 1 + packages/types/build.json | 5 + packages/{globals => types}/package.json | 42 +- .../src/types => types/src}/data-source.ts | 0 .../definitions.ts => types/src/editor.ts} | 72 ++-- .../src/types => types/src}/field-config.ts | 1 - .../{globals/src/types => types/src}/i18n.ts | 0 .../{globals/src/types => types/src}/icon.ts | 0 .../{globals/src/types => types/src}/index.ts | 1 + .../src/types => types/src}/metadata.ts | 0 .../{globals/src/types => types/src}/npm.ts | 0 .../src/types => types/src}/prop-config.ts | 0 .../src/types => types/src}/schema.ts | 0 .../src/types => types/src}/setter-config.ts | 3 +- .../src/types => types/src}/setting-target.ts | 2 +- .../{globals/src/types => types/src}/tip.ts | 0 .../{globals/src/types => types/src}/title.ts | 1 - .../{globals/src/types => types/src}/utils.ts | 0 .../src/types => types/src}/value-type.ts | 0 packages/{globals => types}/tsconfig.json | 4 +- packages/utils/package.json | 0 .../{globals/src/utils => utils/src}/asset.ts | 0 .../src/utils => utils/src}/clone-deep.ts | 0 .../src/utils => utils/src}/create-content.ts | 0 .../src/utils => utils/src}/create-icon.tsx | 4 +- .../src/utils => utils/src}/cursor.less | 0 .../src/utils => utils/src}/cursor.ts | 0 .../utils => utils/src}/get-prototype-of.ts | 0 .../utils => utils/src}/has-own-property.ts | 0 .../{globals/src/utils => utils/src}/index.ts | 12 +- .../src/utils => utils/src}/is-css-url.ts | 0 .../src/utils => utils/src}/is-element.ts | 0 .../src/utils => utils/src}/is-es-module.ts | 0 .../src/utils => utils/src}/is-form-event.ts | 0 .../src/utils => utils/src}/is-function.ts | 0 .../src/utils => utils/src}/is-object.ts | 0 .../utils => utils/src}/is-plain-object.ts | 0 .../src/utils => utils/src}/is-react.ts | 0 .../utils => utils/src}/navtive-selection.ts | 0 .../utils => utils/src}/set-prototype-of.ts | 0 .../src/utils => utils/src}/shallow-equal.ts | 0 .../src/components => utils/src}/svg-icon.tsx | 0 .../src/utils => utils/src}/unique-id.ts | 0 packages/vision-polyfill/src/skeleton/area.ts | 4 +- .../skeleton/components/array-setter/bugs.md | 5 + .../components/array-setter/index.tsx | 279 +++++++++++++ .../components/array-setter/sortable.less | 29 ++ .../components/array-setter/sortable.tsx | 220 ++++++++++ .../components/array-setter/style.less | 103 +++++ .../src/skeleton/components/field/fields.tsx | 184 +++++++++ .../src/skeleton/components/field/index.less | 154 +++++++ .../src/skeleton/components/field/index.ts | 28 ++ .../components/mixed-setter/index.tsx | 257 ++++++++++++ .../components/mixed-setter/style.less | 30 ++ .../components/object-setter/index.tsx | 180 +++++++++ .../components/object-setter/style.less | 31 ++ .../src/skeleton/components/popup/index.tsx | 150 +++++++ .../src/skeleton/components/popup/style.less | 22 + .../src/skeleton/components/register.ts | 29 ++ .../src/skeleton/components/settings/index.ts | 9 + .../src/skeleton/components/settings/main.ts | 89 +++++ .../skeleton/components/settings/package.json | 49 +++ .../components/settings/settings-pane.tsx | 149 +++++++ .../settings/settings-primary-view.tsx | 121 ++++++ .../skeleton/components/settings/style.less | 124 ++++++ .../src/skeleton/components/settings/utils.js | 41 ++ .../{ => components}/widget-views.tsx | 14 +- packages/vision-polyfill/src/skeleton/dock.ts | 4 +- .../src/skeleton/icons/convert.tsx | 16 + .../vision-polyfill/src/skeleton/index.ts | 0 .../skeleton/{ => layouts}/bottom-area.tsx | 4 +- .../src/skeleton/{ => layouts}/left-area.tsx | 2 +- .../{ => layouts}/left-fixed-pane.tsx | 6 +- .../{ => layouts}/left-float-pane.tsx | 4 +- .../src/skeleton/{ => layouts}/main-area.tsx | 6 +- .../src/skeleton/{ => layouts}/right-area.tsx | 4 +- .../src/skeleton/layouts/theme.less | 60 +++ .../src/skeleton/{ => layouts}/toolbar.tsx | 2 +- .../src/skeleton/{ => layouts}/top-area.tsx | 2 +- .../src/skeleton/layouts/workbench.less | 375 ++++++++++++++++++ .../src/skeleton/{ => layouts}/workbench.tsx | 2 +- .../vision-polyfill/src/skeleton/panel.ts | 4 +- .../vision-polyfill/src/skeleton/skeleton.ts | 32 +- .../src/skeleton/transducers/addon-combine.ts | 255 ++++++++++++ .../src/skeleton/transducers/parse-props.ts | 232 +++++++++++ .../src/skeleton/transducers/register.ts | 9 + .../vision-polyfill/src/skeleton/types.ts | 14 +- .../src/skeleton/widget/dialog-dock.ts | 0 .../src/skeleton/widget/dock.ts | 86 ++++ .../src/skeleton/{ => widget}/panel-dock.ts | 7 +- .../src/skeleton/widget/panel.ts | 164 ++++++++ .../src/skeleton/widget/stage.ts | 51 +++ .../src/skeleton/widget/utils.ts | 28 ++ .../src/skeleton/widget/widget-container.ts | 134 +++++++ .../src/skeleton/{ => widget}/widget.ts | 6 +- packages/vision-polyfill/src/vision.ts | 2 +- 294 files changed, 8917 insertions(+), 2992 deletions(-) delete mode 100644 packages/editor-core/CHANGELOG.md rename packages/{globals => editor-core}/cloud-build.json (100%) delete mode 100644 packages/editor-core/src/areaManager.ts delete mode 100644 packages/editor-core/src/context.ts rename packages/{globals => editor-core}/src/di/index.ts (50%) rename packages/{globals => editor-core}/src/di/ioc-context.ts (100%) rename packages/{globals => editor-core}/src/di/setter.ts (90%) rename packages/{globals/src/utils => editor-core/src}/hotkey.ts (100%) rename packages/{globals => editor-core}/src/intl/ali-global-locale.ts (97%) rename packages/{globals/src/intl/index.tsx => editor-core/src/intl/index.ts} (51%) delete mode 100644 packages/editor-core/src/pluginFactory.tsx delete mode 100644 packages/editor-core/src/utils.ts rename packages/editor-core/src/{ => utils}/app-preset.ts (100%) rename packages/{globals => editor-core}/src/utils/get-public-path.ts (100%) create mode 100644 packages/editor-core/src/utils/goldlog.ts create mode 100644 packages/editor-core/src/utils/index.ts rename packages/{globals/src/obx/index.ts => editor-core/src/utils/obx.ts} (100%) rename packages/editor-core/src/{ => utils}/request.ts (91%) rename packages/{globals/src/components => editor-core/src/widgets}/index.ts (63%) create mode 100644 packages/editor-core/src/widgets/tip/index.ts rename packages/{globals/src/components => editor-core/src/widgets}/tip/style.less (100%) rename packages/{globals/src/components => editor-core/src/widgets}/tip/tip-container.tsx (83%) rename packages/{globals/src/components => editor-core/src/widgets}/tip/tip-handler.ts (93%) rename packages/{globals/src/components/tip/tip.tsx => editor-core/src/widgets/tip/tip-item.tsx} (91%) create mode 100644 packages/editor-core/src/widgets/tip/tip.tsx rename packages/{globals/src/components => editor-core/src/widgets}/tip/utils.ts (100%) rename packages/{globals/src/components => editor-core/src/widgets}/title/index.tsx (80%) rename packages/{globals/src/components => editor-core/src/widgets}/title/title.less (100%) create mode 100644 packages/editor-skeleton/src/area.ts delete mode 100644 packages/editor-skeleton/src/components/LeftPlugin/index.scss delete mode 100644 packages/editor-skeleton/src/components/LeftPlugin/index.tsx delete mode 100644 packages/editor-skeleton/src/components/Panel/index.scss delete mode 100644 packages/editor-skeleton/src/components/Panel/index.tsx delete mode 100644 packages/editor-skeleton/src/components/TopIcon/index.scss delete mode 100644 packages/editor-skeleton/src/components/TopIcon/index.tsx delete mode 100644 packages/editor-skeleton/src/components/TopPlugin/index.scss delete mode 100644 packages/editor-skeleton/src/components/TopPlugin/index.tsx create mode 100644 packages/editor-skeleton/src/components/array-setter/bugs.md create mode 100644 packages/editor-skeleton/src/components/array-setter/index.tsx create mode 100644 packages/editor-skeleton/src/components/array-setter/sortable.less create mode 100644 packages/editor-skeleton/src/components/array-setter/sortable.tsx create mode 100644 packages/editor-skeleton/src/components/array-setter/style.less create mode 100644 packages/editor-skeleton/src/components/field/fields.tsx create mode 100644 packages/editor-skeleton/src/components/field/index.less create mode 100644 packages/editor-skeleton/src/components/field/index.ts create mode 100644 packages/editor-skeleton/src/components/mixed-setter/index.tsx create mode 100644 packages/editor-skeleton/src/components/mixed-setter/style.less create mode 100644 packages/editor-skeleton/src/components/object-setter/index.tsx create mode 100644 packages/editor-skeleton/src/components/object-setter/style.less create mode 100644 packages/editor-skeleton/src/components/popup/index.tsx create mode 100644 packages/editor-skeleton/src/components/popup/style.less create mode 100644 packages/editor-skeleton/src/components/settings/index.ts create mode 100644 packages/editor-skeleton/src/components/settings/main.ts create mode 100644 packages/editor-skeleton/src/components/settings/package.json create mode 100644 packages/editor-skeleton/src/components/settings/register.ts create mode 100644 packages/editor-skeleton/src/components/settings/settings-pane.tsx create mode 100644 packages/editor-skeleton/src/components/settings/settings-primary-view.tsx create mode 100644 packages/editor-skeleton/src/components/settings/style.less create mode 100644 packages/editor-skeleton/src/components/settings/transducers/addon-combine.ts create mode 100644 packages/editor-skeleton/src/components/settings/transducers/parse-props.ts create mode 100644 packages/editor-skeleton/src/components/settings/transducers/register.ts create mode 100644 packages/editor-skeleton/src/components/settings/utils.js create mode 100644 packages/editor-skeleton/src/components/widget-views.tsx delete mode 100644 packages/editor-skeleton/src/config/skeleton.ts delete mode 100644 packages/editor-skeleton/src/config/utils.ts create mode 100644 packages/editor-skeleton/src/dock.ts delete mode 100644 packages/editor-skeleton/src/global.scss create mode 100644 packages/editor-skeleton/src/icons/convert.tsx delete mode 100644 packages/editor-skeleton/src/layouts/CenterArea/index.scss delete mode 100644 packages/editor-skeleton/src/layouts/CenterArea/index.tsx delete mode 100644 packages/editor-skeleton/src/layouts/LeftArea/index.scss delete mode 100644 packages/editor-skeleton/src/layouts/LeftArea/index.tsx delete mode 100644 packages/editor-skeleton/src/layouts/LeftArea/nav.tsx delete mode 100644 packages/editor-skeleton/src/layouts/LeftArea/panel.tsx delete mode 100644 packages/editor-skeleton/src/layouts/RightArea/index.scss delete mode 100644 packages/editor-skeleton/src/layouts/RightArea/index.tsx delete mode 100644 packages/editor-skeleton/src/layouts/TopArea/index.scss delete mode 100644 packages/editor-skeleton/src/layouts/TopArea/index.tsx create mode 100644 packages/editor-skeleton/src/layouts/bottom-area.tsx create mode 100644 packages/editor-skeleton/src/layouts/left-area.tsx create mode 100644 packages/editor-skeleton/src/layouts/left-fixed-pane.tsx create mode 100644 packages/editor-skeleton/src/layouts/left-float-pane.tsx create mode 100644 packages/editor-skeleton/src/layouts/main-area.tsx create mode 100644 packages/editor-skeleton/src/layouts/right-area.tsx rename packages/{vision-polyfill/src/skeleton => editor-skeleton/src/layouts}/theme.less (100%) create mode 100644 packages/editor-skeleton/src/layouts/toolbar.tsx create mode 100644 packages/editor-skeleton/src/layouts/top-area.tsx rename packages/{vision-polyfill/src/skeleton => editor-skeleton/src/layouts}/workbench.less (100%) create mode 100644 packages/editor-skeleton/src/layouts/workbench.tsx delete mode 100644 packages/editor-skeleton/src/locale/en-US.js delete mode 100644 packages/editor-skeleton/src/locale/ja-JP.js delete mode 100644 packages/editor-skeleton/src/locale/zh-CN.js delete mode 100644 packages/editor-skeleton/src/locale/zh-TW.js create mode 100644 packages/editor-skeleton/src/panel.ts create mode 100644 packages/editor-skeleton/src/skeleton.ts delete mode 100644 packages/editor-skeleton/src/skeleton.tsx create mode 100644 packages/editor-skeleton/src/types.ts rename packages/{vision-polyfill/src/skeleton => editor-skeleton/src/widget}/dialog-dock.ts (100%) create mode 100644 packages/editor-skeleton/src/widget/dock.ts create mode 100644 packages/editor-skeleton/src/widget/panel-dock.ts create mode 100644 packages/editor-skeleton/src/widget/panel.ts rename packages/{vision-polyfill/src/skeleton => editor-skeleton/src/widget}/stage.ts (92%) rename packages/{vision-polyfill/src/skeleton => editor-skeleton/src/widget}/utils.ts (100%) rename packages/{vision-polyfill/src/skeleton => editor-skeleton/src/widget}/widget-container.ts (100%) create mode 100644 packages/editor-skeleton/src/widget/widget.ts delete mode 100644 packages/globals/CHANGELOG.md delete mode 100644 packages/globals/README.md delete mode 100644 packages/globals/build.json delete mode 100644 packages/globals/src/components/tip/embed-tip.tsx delete mode 100644 packages/globals/src/components/tip/index.ts delete mode 100644 packages/globals/src/di/editor.ts delete mode 100644 packages/globals/src/di/transducer.ts delete mode 100644 packages/globals/src/index.ts create mode 100644 packages/types/README.md create mode 100644 packages/types/build.json rename packages/{globals => types}/package.json (66%) rename packages/{globals/src/types => types/src}/data-source.ts (100%) rename packages/{editor-core/src/definitions.ts => types/src/editor.ts} (60%) rename packages/{globals/src/types => types/src}/field-config.ts (99%) rename packages/{globals/src/types => types/src}/i18n.ts (100%) rename packages/{globals/src/types => types/src}/icon.ts (100%) rename packages/{globals/src/types => types/src}/index.ts (93%) rename packages/{globals/src/types => types/src}/metadata.ts (100%) rename packages/{globals/src/types => types/src}/npm.ts (100%) rename packages/{globals/src/types => types/src}/prop-config.ts (100%) rename packages/{globals/src/types => types/src}/schema.ts (100%) rename packages/{globals/src/types => types/src}/setter-config.ts (96%) rename packages/{globals/src/types => types/src}/setting-target.ts (96%) rename packages/{globals/src/types => types/src}/tip.ts (100%) rename packages/{globals/src/types => types/src}/title.ts (99%) rename packages/{globals/src/types => types/src}/utils.ts (100%) rename packages/{globals/src/types => types/src}/value-type.ts (100%) rename packages/{globals => types}/tsconfig.json (73%) create mode 100644 packages/utils/package.json rename packages/{globals/src/utils => utils/src}/asset.ts (100%) rename packages/{globals/src/utils => utils/src}/clone-deep.ts (100%) rename packages/{globals/src/utils => utils/src}/create-content.ts (100%) rename packages/{globals/src/utils => utils/src}/create-icon.tsx (93%) rename packages/{globals/src/utils => utils/src}/cursor.less (100%) rename packages/{globals/src/utils => utils/src}/cursor.ts (100%) rename packages/{globals/src/utils => utils/src}/get-prototype-of.ts (100%) rename packages/{globals/src/utils => utils/src}/has-own-property.ts (100%) rename packages/{globals/src/utils => utils/src}/index.ts (86%) rename packages/{globals/src/utils => utils/src}/is-css-url.ts (100%) rename packages/{globals/src/utils => utils/src}/is-element.ts (100%) rename packages/{globals/src/utils => utils/src}/is-es-module.ts (100%) rename packages/{globals/src/utils => utils/src}/is-form-event.ts (100%) rename packages/{globals/src/utils => utils/src}/is-function.ts (100%) rename packages/{globals/src/utils => utils/src}/is-object.ts (100%) rename packages/{globals/src/utils => utils/src}/is-plain-object.ts (100%) rename packages/{globals/src/utils => utils/src}/is-react.ts (100%) rename packages/{globals/src/utils => utils/src}/navtive-selection.ts (100%) rename packages/{globals/src/utils => utils/src}/set-prototype-of.ts (100%) rename packages/{globals/src/utils => utils/src}/shallow-equal.ts (100%) rename packages/{globals/src/components => utils/src}/svg-icon.tsx (100%) rename packages/{globals/src/utils => utils/src}/unique-id.ts (100%) create mode 100644 packages/vision-polyfill/src/skeleton/components/array-setter/bugs.md create mode 100644 packages/vision-polyfill/src/skeleton/components/array-setter/index.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/array-setter/sortable.less create mode 100644 packages/vision-polyfill/src/skeleton/components/array-setter/sortable.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/array-setter/style.less create mode 100644 packages/vision-polyfill/src/skeleton/components/field/fields.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/field/index.less create mode 100644 packages/vision-polyfill/src/skeleton/components/field/index.ts create mode 100644 packages/vision-polyfill/src/skeleton/components/mixed-setter/index.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/mixed-setter/style.less create mode 100644 packages/vision-polyfill/src/skeleton/components/object-setter/index.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/object-setter/style.less create mode 100644 packages/vision-polyfill/src/skeleton/components/popup/index.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/popup/style.less create mode 100644 packages/vision-polyfill/src/skeleton/components/register.ts create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/index.ts create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/main.ts create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/package.json create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/settings-pane.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/settings-primary-view.tsx create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/style.less create mode 100644 packages/vision-polyfill/src/skeleton/components/settings/utils.js rename packages/vision-polyfill/src/skeleton/{ => components}/widget-views.tsx (94%) create mode 100644 packages/vision-polyfill/src/skeleton/icons/convert.tsx create mode 100644 packages/vision-polyfill/src/skeleton/index.ts rename packages/vision-polyfill/src/skeleton/{ => layouts}/bottom-area.tsx (92%) rename packages/vision-polyfill/src/skeleton/{ => layouts}/left-area.tsx (97%) rename packages/vision-polyfill/src/skeleton/{ => layouts}/left-fixed-pane.tsx (91%) rename packages/vision-polyfill/src/skeleton/{ => layouts}/left-float-pane.tsx (96%) rename packages/vision-polyfill/src/skeleton/{ => layouts}/main-area.tsx (81%) rename packages/vision-polyfill/src/skeleton/{ => layouts}/right-area.tsx (91%) create mode 100644 packages/vision-polyfill/src/skeleton/layouts/theme.less rename packages/vision-polyfill/src/skeleton/{ => layouts}/toolbar.tsx (97%) rename packages/vision-polyfill/src/skeleton/{ => layouts}/top-area.tsx (97%) create mode 100644 packages/vision-polyfill/src/skeleton/layouts/workbench.less rename packages/vision-polyfill/src/skeleton/{ => layouts}/workbench.tsx (96%) create mode 100644 packages/vision-polyfill/src/skeleton/transducers/addon-combine.ts create mode 100644 packages/vision-polyfill/src/skeleton/transducers/parse-props.ts create mode 100644 packages/vision-polyfill/src/skeleton/transducers/register.ts create mode 100644 packages/vision-polyfill/src/skeleton/widget/dialog-dock.ts create mode 100644 packages/vision-polyfill/src/skeleton/widget/dock.ts rename packages/vision-polyfill/src/skeleton/{ => widget}/panel-dock.ts (92%) create mode 100644 packages/vision-polyfill/src/skeleton/widget/panel.ts create mode 100644 packages/vision-polyfill/src/skeleton/widget/stage.ts create mode 100644 packages/vision-polyfill/src/skeleton/widget/utils.ts create mode 100644 packages/vision-polyfill/src/skeleton/widget/widget-container.ts rename packages/vision-polyfill/src/skeleton/{ => widget}/widget.ts (92%) diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx index 715023181..63201f31c 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx @@ -1,8 +1,9 @@ import { Component, Fragment, PureComponent } from 'react'; import classNames from 'classnames'; -import { computed, observer, TitleContent, Title } from '@ali/lowcode-globals'; +import { computed, observer, Title } from '@ali/lowcode-editor-core'; import { SimulatorContext } from '../context'; import { BuiltinSimulatorHost } from '../host'; +import { TitleContent } from '@ali/lowcode-types'; export class BorderHoveringInstance extends PureComponent<{ title: TitleContent; diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx index dfb41f797..5597e9382 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx @@ -9,19 +9,13 @@ import { ComponentType, } from 'react'; import classNames from 'classnames'; -import { - observer, - computed, - createIcon, - EmbedTip, - isReactComponent, - ActionContentObject, - isActionContentObject, -} from '@ali/lowcode-globals'; +import { observer, computed, Tip } from '@ali/lowcode-editor-core'; +import { createIcon, isReactComponent } from '@ali/lowcode-utils'; +import { ActionContentObject, isActionContentObject } from '@ali/lowcode-types'; import { SimulatorContext } from '../context'; import { BuiltinSimulatorHost } from '../host'; -import { OffsetObserver } from '../../../designer'; -import { Node } from '../../../document'; +import { OffsetObserver } from '../../designer'; +import { Node } from '../../document'; @observer export class BorderSelectingInstance extends Component<{ @@ -94,9 +88,9 @@ class Toolbar extends Component<{ observed: OffsetObserver }> { } const { node } = observed; const actions: ReactNodeArray = []; - node.componentMeta.availableActions.forEach(action => { + node.componentMeta.availableActions.forEach((action) => { const { important, condition, content, name } = action; - if (node.isSlotRoot && (name === 'copy' || name === 'remove')) { + if (node.isSlot() && (name === 'copy' || name === 'remove')) { // FIXME: need this? return; } @@ -130,7 +124,7 @@ function createAction(content: ReactNode | ComponentType | ActionContentObj }} > {icon && createIcon(icon)} - {title} + {title} ); } @@ -167,7 +161,7 @@ export class BorderSelectingForNode extends Component<{ node: Node }> { } return ( - {instances.map(instance => { + {instances.map((instance) => { const observed = designer.createOffsetObserver({ node, instance, @@ -216,7 +210,7 @@ export class BorderSelecting extends Component { return ( - {selecting.map(node => ( + {selecting.map((node) => ( ))} diff --git a/packages/designer/src/builtin-simulator/bem-tools/index.tsx b/packages/designer/src/builtin-simulator/bem-tools/index.tsx index 8ed26d18e..0ad81c8c4 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/index.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/index.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { observer } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; import { BorderHovering } from './border-hovering'; import { SimulatorContext } from '../context'; import { BuiltinSimulatorHost } from '../host'; diff --git a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx index f0c546b99..f630e3547 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { computed, observer } from '@ali/lowcode-globals'; +import { computed, observer } from '@ali/lowcode-editor-core'; import { SimulatorContext } from '../context'; import { BuiltinSimulatorHost } from '../host'; import { @@ -10,7 +10,7 @@ import { isVertical } from '../../designer'; import { ISimulatorHost, } from '../../simulator'; -import {ParentalNode } from '../../document'; +import { ParentalNode } from '../../document'; import './insertion.less'; interface InsertionData { diff --git a/packages/designer/src/builtin-simulator/create-simulator.ts b/packages/designer/src/builtin-simulator/create-simulator.ts index ea772db25..796b10f01 100644 --- a/packages/designer/src/builtin-simulator/create-simulator.ts +++ b/packages/designer/src/builtin-simulator/create-simulator.ts @@ -1,7 +1,7 @@ // NOTE: 仅用作类型标注,切勿作为实体使用 import { BuiltinSimulatorHost } from './host'; -import { AssetLevel, AssetLevels, AssetList, isAssetBundle, isAssetItem, AssetType, assetItem } from '@ali/lowcode-globals'; -import { isCSSUrl } from '@ali/lowcode-globals'; +import { AssetLevel, AssetLevels, AssetList, isAssetBundle, isAssetItem, AssetType, assetItem } from '@ali/lowcode-utils'; +import { isCSSUrl } from '@ali/lowcode-utils'; import { BuiltinSimulatorRenderer } from './renderer'; export function createSimulator( diff --git a/packages/designer/src/builtin-simulator/host-view.tsx b/packages/designer/src/builtin-simulator/host-view.tsx index 756653f15..6d71ce9e3 100644 --- a/packages/designer/src/builtin-simulator/host-view.tsx +++ b/packages/designer/src/builtin-simulator/host-view.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { observer } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; import { BuiltinSimulatorHost, BuiltinSimulatorProps } from './host'; import { DocumentModel } from '../document'; import { SimulatorContext } from './context'; diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index 0dbddf5f1..5ac616dc2 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -1,10 +1,10 @@ -import { obx, autorun, computed } from '@ali/lowcode-globals'; +import { obx, autorun, computed, getPublicPath, hotkey } from '@ali/lowcode-editor-core'; import { ISimulatorHost, Component, NodeInstance, ComponentInstance } from '../simulator'; import Viewport from './viewport'; import { createSimulator } from './create-simulator'; import { Node, ParentalNode, DocumentModel, isNode, contains, isRootNode } from '../document'; import ResourceConsumer from './resource-consumer'; -import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType, getPublicPath } from '@ali/lowcode-globals'; +import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType, isElement } from '@ali/lowcode-utils'; import { DragObjectType, isShaken, @@ -21,9 +21,8 @@ import { Rect, CanvasPoint, } from '../designer'; -import { parseProps, parseMetadata } from './utils/parse-metadata'; -import { isElement, hotkey } from '@ali/lowcode-globals'; -import { ComponentMetadata } from '@ali/lowcode-globals'; +import { parseMetadata } from './utils/parse-metadata'; +import { ComponentMetadata } from '@ali/lowcode-types'; import { BuiltinSimulatorRenderer } from './renderer'; import clipboard from '../designer/clipboard'; @@ -967,7 +966,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost action.name === name); + const i = builtinComponentActions.findIndex((action) => action.name === name); if (i > -1) { builtinComponentActions.splice(i, 1); } @@ -308,3 +309,33 @@ export function removeBuiltinComponentAction(name: string) { export function addBuiltinComponentAction(action: ComponentAction) { builtinComponentActions.push(action); } + +export interface MetadataTransducer { + (prev: TransformedComponentMetadata): TransformedComponentMetadata; + /** + * 0 - 9 system + * 10 - 99 builtin-plugin + * 100 - app & plugin + */ + level?: number; + /** + * use to replace TODO + */ + id?: string; +} +const metadataTransducers: MetadataTransducer[] = []; + +export function registerMetadataTransducer(transducer: MetadataTransducer, level: number = 100, id?: string) { + transducer.level = level; + transducer.id = id; + const i = metadataTransducers.findIndex((item) => item.level != null && item.level > level); + if (i < 0) { + metadataTransducers.push(transducer); + } else { + metadataTransducers.splice(i, 0, transducer); + } +} + +export function getRegisteredMetadataTransducers(): MetadataTransducer[] { + return metadataTransducers; +} diff --git a/packages/designer/src/designer/builtin-hotkey.ts b/packages/designer/src/designer/builtin-hotkey.ts index 08a13be28..d11edbe03 100644 --- a/packages/designer/src/designer/builtin-hotkey.ts +++ b/packages/designer/src/designer/builtin-hotkey.ts @@ -1,6 +1,7 @@ -import { hotkey, isFormEvent } from '@ali/lowcode-globals'; +import { hotkey } from '@ali/lowcode-editor-core'; +import { isFormEvent } from '@ali/lowcode-utils'; import { focusing } from './focusing'; -import { insertChildren } from '../document'; +import { insertChildren, TransformStage } from '../document'; import clipboard from './clipboard'; // hotkey binding @@ -52,7 +53,7 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => { if (!selected || selected.length < 1) return; const componentsMap = {}; - const componentsTree = selected.map((item) => item.export(false)); + const componentsTree = selected.map((item) => item.export(TransformStage.Save)); const data = { type: 'nodeSchema', componentsMap, componentsTree }; diff --git a/packages/designer/src/designer/designer-view.tsx b/packages/designer/src/designer/designer-view.tsx index ec8d46ea1..fa601a994 100644 --- a/packages/designer/src/designer/designer-view.tsx +++ b/packages/designer/src/designer/designer-view.tsx @@ -1,6 +1,5 @@ import { Component } from 'react'; import classNames from 'classnames'; -import { TipContainer } from '@ali/lowcode-globals'; import BuiltinDragGhostComponent from './drag-ghost'; import { Designer, DesignerProps } from './designer'; import { ProjectView } from '../project'; diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index 1e1ce573d..bc0a7bf83 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -1,16 +1,14 @@ import { ComponentType } from 'react'; +import { obx, computed, autorun } from '@ali/lowcode-editor-core'; import { ProjectSchema, ComponentMetadata, ComponentAction, NpmInfo, - obx, - computed, - autorun, IEditor, CompositeObject, PropsList, -} from '@ali/lowcode-globals'; +} from '@ali/lowcode-types'; import { Project } from '../project'; import { Node, DocumentModel, insertChildren, isRootNode, ParentalNode, TransformStage } from '../document'; import { ComponentMeta } from '../component-meta'; diff --git a/packages/designer/src/designer/drag-ghost/index.tsx b/packages/designer/src/designer/drag-ghost/index.tsx index 407548610..d32ee526e 100644 --- a/packages/designer/src/designer/drag-ghost/index.tsx +++ b/packages/designer/src/designer/drag-ghost/index.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { observer, obx, Title } from '@ali/lowcode-globals'; +import { observer, obx, Title } from '@ali/lowcode-editor-core'; import { Designer } from '../designer'; import { DragObject, isDragNodeObject, isDragNodeDataObject } from '../dragon'; import './ghost.less'; diff --git a/packages/designer/src/designer/dragon.ts b/packages/designer/src/designer/dragon.ts index 58c14acea..f58adc19a 100644 --- a/packages/designer/src/designer/dragon.ts +++ b/packages/designer/src/designer/dragon.ts @@ -1,11 +1,11 @@ import { EventEmitter } from 'events'; -import { NodeSchema, obx } from '@ali/lowcode-globals'; +import { obx } from '@ali/lowcode-editor-core'; +import { NodeSchema } from '@ali/lowcode-types'; +import { setNativeSelection, cursor } from '@ali/lowcode-utils'; import { DropLocation } from './location'; import { Node, DocumentModel } from '../document'; import { ISimulatorHost, isSimulatorHost } from '../simulator'; import { Designer } from './designer'; -import { setNativeSelection } from '@ali/lowcode-globals'; -import { cursor } from '@ali/lowcode-globals'; export interface LocateEvent { readonly type: 'LocateEvent'; diff --git a/packages/designer/src/designer/hovering.ts b/packages/designer/src/designer/hovering.ts index 98ce9b2fd..9c27801a7 100644 --- a/packages/designer/src/designer/hovering.ts +++ b/packages/designer/src/designer/hovering.ts @@ -1,4 +1,4 @@ -import { obx } from '@ali/lowcode-globals'; +import { obx } from '@ali/lowcode-editor-core'; import { Node, DocumentModel } from '../document'; export class Hovering { diff --git a/packages/designer/src/designer/offset-observer.ts b/packages/designer/src/designer/offset-observer.ts index 1105b01be..8793317f9 100644 --- a/packages/designer/src/designer/offset-observer.ts +++ b/packages/designer/src/designer/offset-observer.ts @@ -1,5 +1,5 @@ -import { obx, computed } from '@ali/lowcode-globals'; -import { uniqueId } from '@ali/lowcode-globals'; +import { obx, computed } from '@ali/lowcode-editor-core'; +import { uniqueId } from '@ali/lowcode-utils'; import { INodeSelector, IViewport } from '../simulator'; import { isRootNode, Node } from '../document'; diff --git a/packages/designer/src/designer/scroller.ts b/packages/designer/src/designer/scroller.ts index a3c90a46c..a7c93b91f 100644 --- a/packages/designer/src/designer/scroller.ts +++ b/packages/designer/src/designer/scroller.ts @@ -1,4 +1,4 @@ -import { isElement } from '@ali/lowcode-globals'; +import { isElement } from '@ali/lowcode-utils'; export class ScrollTarget { get left() { diff --git a/packages/designer/src/designer/setting/setting-entry.ts b/packages/designer/src/designer/setting/setting-entry.ts index 9ab5831db..8a81b5629 100644 --- a/packages/designer/src/designer/setting/setting-entry.ts +++ b/packages/designer/src/designer/setting/setting-entry.ts @@ -1,4 +1,4 @@ -import { SettingTarget } from '@ali/lowcode-globals'; +import { SettingTarget } from '@ali/lowcode-types'; import { ComponentMeta } from '../../component-meta'; import { Designer } from '../designer'; import { Node } from '../../document'; diff --git a/packages/designer/src/designer/setting/setting-field.ts b/packages/designer/src/designer/setting/setting-field.ts index 2aa748480..e40100ed8 100644 --- a/packages/designer/src/designer/setting/setting-field.ts +++ b/packages/designer/src/designer/setting/setting-field.ts @@ -1,7 +1,8 @@ -import { TitleContent, computed, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView, obx } from '@ali/lowcode-globals'; +import { TitleContent, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView } from '@ali/lowcode-types'; import { Transducer } from './utils'; import { SettingPropEntry } from './setting-prop-entry'; import { SettingEntry } from './setting-entry'; +import { computed, obx } from '@ali/lowcode-editor-core'; export class SettingField extends SettingPropEntry implements SettingEntry { readonly isSettingField = true; diff --git a/packages/designer/src/designer/setting/setting-prop-entry.ts b/packages/designer/src/designer/setting/setting-prop-entry.ts index 87eae902a..34bf07f9b 100644 --- a/packages/designer/src/designer/setting/setting-prop-entry.ts +++ b/packages/designer/src/designer/setting/setting-prop-entry.ts @@ -1,4 +1,6 @@ -import { obx, uniqueId, computed, IEditor } from '@ali/lowcode-globals'; +import { obx, computed } from '@ali/lowcode-editor-core'; +import { IEditor } from '@ali/lowcode-types'; +import { uniqueId } from '@ali/lowcode-utils'; import { SettingEntry } from './setting-entry'; import { Node } from '../../document'; import { ComponentMeta } from '../../component-meta'; diff --git a/packages/designer/src/designer/setting/setting-top-entry.ts b/packages/designer/src/designer/setting/setting-top-entry.ts index 759ece7fe..8595861d4 100644 --- a/packages/designer/src/designer/setting/setting-top-entry.ts +++ b/packages/designer/src/designer/setting/setting-top-entry.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; -import { CustomView, computed, isCustomView, IEditor } from '@ali/lowcode-globals'; +import { CustomView, isCustomView, IEditor } from '@ali/lowcode-types'; +import { computed } from '@ali/lowcode-editor-core'; import { SettingEntry } from './setting-entry'; import { SettingField } from './setting-field'; import { SettingPropEntry } from './setting-prop-entry'; diff --git a/packages/designer/src/document/document-model.ts b/packages/designer/src/document/document-model.ts index c5cd064d8..6e8c37e24 100644 --- a/packages/designer/src/document/document-model.ts +++ b/packages/designer/src/document/document-model.ts @@ -1,17 +1,5 @@ -import { - NodeData, - isJSExpression, - isDOMText, - NodeSchema, - computed, - obx, - autorun, - isNodeSchema, - uniqueId, - PageSchema, - ComponentSchema, - RootSchema, -} from '@ali/lowcode-globals'; +import { computed, obx } from '@ali/lowcode-editor-core'; +import { NodeData, isJSExpression, isDOMText, NodeSchema, isNodeSchema, RootSchema } from '@ali/lowcode-types'; import { Project } from '../project'; import { ISimulatorHost } from '../simulator'; import { ComponentMeta } from '../component-meta'; @@ -20,6 +8,7 @@ import { Node, insertChildren, insertChild, isNode, RootNode, ParentalNode } fro import { Selection } from './selection'; import { History } from './history'; import { TransformStage } from './node'; +import { uniqueId } from '@ali/lowcode-utils'; export type GetDataType = T extends undefined ? NodeType extends { @@ -90,11 +79,13 @@ export class DocumentModel { this._blank = true; } - this.rootNode = this.createNode(schema || { - componentName: 'Page', - id: 'root', - fileName: '' - }); + this.rootNode = this.createNode( + schema || { + componentName: 'Page', + id: 'root', + fileName: '', + }, + ); this.history = new History( () => this.schema, diff --git a/packages/designer/src/document/document-view.tsx b/packages/designer/src/document/document-view.tsx index b9771907f..2565f34fc 100644 --- a/packages/designer/src/document/document-view.tsx +++ b/packages/designer/src/document/document-view.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; import classNames from 'classnames'; -import { observer } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; import { DocumentModel } from './document-model'; import { BuiltinSimulatorHostView } from '../builtin-simulator'; diff --git a/packages/designer/src/document/history.ts b/packages/designer/src/document/history.ts index 8c6b67e2a..0f283b65b 100644 --- a/packages/designer/src/document/history.ts +++ b/packages/designer/src/document/history.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'events'; -import { NodeSchema, autorun, Reaction, untracked } from '@ali/lowcode-globals'; +import { autorun, Reaction, untracked } from '@ali/lowcode-editor-core'; +import { NodeSchema } from '@ali/lowcode-types'; // TODO: cache to localStorage diff --git a/packages/designer/src/document/node/exclusive-group.ts b/packages/designer/src/document/node/exclusive-group.ts index 8e3f7b927..cd4b46030 100644 --- a/packages/designer/src/document/node/exclusive-group.ts +++ b/packages/designer/src/document/node/exclusive-group.ts @@ -1,5 +1,6 @@ -import { obx, computed, TitleContent } from '@ali/lowcode-globals'; -import { uniqueId } from '@ali/lowcode-globals'; +import { obx, computed } from '@ali/lowcode-editor-core'; +import { uniqueId } from '@ali/lowcode-utils'; +import { TitleContent } from '@ali/lowcode-types'; import { Node } from './node'; import { intl } from '../../locale'; diff --git a/packages/designer/src/document/node/node-children.ts b/packages/designer/src/document/node/node-children.ts index 712ed1374..a8a4a364e 100644 --- a/packages/designer/src/document/node/node-children.ts +++ b/packages/designer/src/document/node/node-children.ts @@ -1,6 +1,7 @@ -import { NodeData, isNodeSchema, obx, computed } from '@ali/lowcode-globals'; +import { obx, computed } from '@ali/lowcode-editor-core'; import { Node, ParentalNode } from './node'; import { TransformStage } from './transform-stage'; +import { NodeData, isNodeSchema } from '@ali/lowcode-types'; export class NodeChildren { @obx.val private children: Node[]; diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index 6f8fe58d8..a4bb4c21a 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -1,3 +1,4 @@ +import { obx, computed } from '@ali/lowcode-editor-core'; import { isDOMText, isJSExpression, @@ -6,12 +7,10 @@ import { PropsList, NodeData, TitleContent, - obx, - computed, SlotSchema, PageSchema, ComponentSchema, -} from '@ali/lowcode-globals'; +} from '@ali/lowcode-types'; import { Props, EXTRA_KEY_PREFIX } from './props/props'; import { DocumentModel } from '../document-model'; import { NodeChildren } from './node-children'; @@ -20,8 +19,6 @@ import { ComponentMeta } from '../../component-meta'; import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group'; import { TransformStage } from './transform-stage'; - - /** * 基础节点 * @@ -175,7 +172,7 @@ export class Node { } isRoot(): this is RootNode { - return this.document.rootNode == this as any; + return this.document.rootNode == (this as any); } isPage(): this is PageNode { @@ -298,7 +295,7 @@ export class Node { @computed get slots() { // TODO: optimize recore/obx, array maked every time, donot as changed const slots: Node[] = []; - this.props.forEach(item => { + this.props.forEach((item) => { if (item.type === 'slot') { slots.push(item.slotNode!); } @@ -563,7 +560,7 @@ export class Node { this.children?.insert(node, ref ? ref.index : null); } insertAfter(node: Node, ref?: Node) { - this.children?.insert(node, ref ? (ref.index + 1) : null); + this.children?.insert(node, ref ? ref.index + 1 : null); } getParent() { return this.parent; @@ -594,9 +591,7 @@ export class Node { /** * @deprecated */ - setStatus() { - - } + setStatus() {} /** * @deprecated */ diff --git a/packages/designer/src/document/node/props/prop-stash.ts b/packages/designer/src/document/node/props/prop-stash.ts index ef57a8afe..0cf754f92 100644 --- a/packages/designer/src/document/node/props/prop-stash.ts +++ b/packages/designer/src/document/node/props/prop-stash.ts @@ -1,4 +1,4 @@ -import { obx, autorun, untracked, computed } from '@ali/lowcode-globals'; +import { obx, autorun, untracked, computed } from '@ali/lowcode-editor-core'; import { Prop, IPropParent, UNSET } from './prop'; import { Props } from './props'; diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index f82eb013a..fcb37a8a3 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -1,16 +1,6 @@ -import { - CompositeValue, - isJSExpression, - isJSSlot, - untracked, - computed, - obx, - JSSlot, - SlotSchema -} from '@ali/lowcode-globals'; -import { uniqueId } from '@ali/lowcode-globals'; -import { isPlainObject } from '@ali/lowcode-globals'; -import { hasOwnProperty } from '@ali/lowcode-globals'; +import { untracked, computed, obx } from '@ali/lowcode-editor-core'; +import { CompositeValue, isJSExpression, isJSSlot, JSSlot, SlotSchema } from '@ali/lowcode-types'; +import { uniqueId, isPlainObject, hasOwnProperty } from '@ali/lowcode-utils'; import { PropStash } from './prop-stash'; import { valueToSource } from './value-to-source'; import { Props } from './props'; diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts index bfdead44b..c9d61056a 100644 --- a/packages/designer/src/document/node/props/props.ts +++ b/packages/designer/src/document/node/props/props.ts @@ -1,10 +1,12 @@ -import { PropsMap, PropsList, CompositeValue, computed, obx, uniqueId } from '@ali/lowcode-globals'; +import { computed, obx } from '@ali/lowcode-editor-core'; +import { PropsMap, PropsList, CompositeValue } from '@ali/lowcode-types'; +import { uniqueId } from '@ali/lowcode-utils'; import { PropStash } from './prop-stash'; import { Prop, IPropParent, UNSET } from './prop'; import { Node } from '../node'; import { TransformStage } from '../transform-stage'; -export const EXTRA_KEY_PREFIX = '__'; +export const EXTRA_KEY_PREFIX = '___'; export class Props implements IPropParent { readonly id = uniqueId('props'); diff --git a/packages/designer/src/document/selection.ts b/packages/designer/src/document/selection.ts index caa4e4cef..f1741b48b 100644 --- a/packages/designer/src/document/selection.ts +++ b/packages/designer/src/document/selection.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { obx } from '@ali/lowcode-globals'; +import { obx } from '@ali/lowcode-editor-core'; import { Node, comparePosition, PositionNO } from './node/node'; import { DocumentModel } from './document-model'; diff --git a/packages/designer/src/icons/clone.tsx b/packages/designer/src/icons/clone.tsx index 374f061df..844b3f5b0 100644 --- a/packages/designer/src/icons/clone.tsx +++ b/packages/designer/src/icons/clone.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from "@ali/lowcode-globals"; +import { SVGIcon, IconProps } from "@ali/lowcode-utils"; export function IconClone(props: IconProps) { return ( diff --git a/packages/designer/src/icons/component.tsx b/packages/designer/src/icons/component.tsx index 26b99e902..78a3ba1f0 100644 --- a/packages/designer/src/icons/component.tsx +++ b/packages/designer/src/icons/component.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from "@ali/lowcode-globals"; +import { SVGIcon, IconProps } from "@ali/lowcode-utils"; export function IconComponent(props: IconProps) { return ( diff --git a/packages/designer/src/icons/container.tsx b/packages/designer/src/icons/container.tsx index 62a8cffba..5c46b3f18 100644 --- a/packages/designer/src/icons/container.tsx +++ b/packages/designer/src/icons/container.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from "@ali/lowcode-globals"; +import { SVGIcon, IconProps } from "@ali/lowcode-utils"; export function IconContainer(props: IconProps) { return ( diff --git a/packages/designer/src/icons/hidden.tsx b/packages/designer/src/icons/hidden.tsx index e3a001ca4..0f944cadf 100644 --- a/packages/designer/src/icons/hidden.tsx +++ b/packages/designer/src/icons/hidden.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from "@ali/lowcode-globals"; +import { SVGIcon, IconProps } from "@ali/lowcode-utils"; export function IconHidden(props: IconProps) { return ( diff --git a/packages/designer/src/icons/page.tsx b/packages/designer/src/icons/page.tsx index 1762b3d94..a957239fe 100644 --- a/packages/designer/src/icons/page.tsx +++ b/packages/designer/src/icons/page.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from "@ali/lowcode-globals"; +import { SVGIcon, IconProps } from "@ali/lowcode-utils"; export function IconPage(props: IconProps) { return ( diff --git a/packages/designer/src/icons/remove.tsx b/packages/designer/src/icons/remove.tsx index e10bd8e04..7de39eadb 100644 --- a/packages/designer/src/icons/remove.tsx +++ b/packages/designer/src/icons/remove.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconRemove(props: IconProps) { return ( diff --git a/packages/designer/src/icons/setting.tsx b/packages/designer/src/icons/setting.tsx index d928bda86..4604079cd 100644 --- a/packages/designer/src/icons/setting.tsx +++ b/packages/designer/src/icons/setting.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconSetting(props: IconProps) { return ( diff --git a/packages/designer/src/locale/index.ts b/packages/designer/src/locale/index.ts index 913dd42f8..dfd17521f 100644 --- a/packages/designer/src/locale/index.ts +++ b/packages/designer/src/locale/index.ts @@ -1,4 +1,4 @@ -import { createIntl } from '@ali/lowcode-globals'; +import { createIntl } from '@ali/lowcode-editor-core'; import en_US from './en-US.json'; import zh_CN from './zh-CN.json'; diff --git a/packages/designer/src/project/project-view.tsx b/packages/designer/src/project/project-view.tsx index cfec45792..649358b62 100644 --- a/packages/designer/src/project/project-view.tsx +++ b/packages/designer/src/project/project-view.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { observer } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; import { Designer } from '../designer'; import { DocumentView } from '../document'; import { intl } from '../locale'; diff --git a/packages/designer/src/project/project.ts b/packages/designer/src/project/project.ts index 019c5e780..8217697be 100644 --- a/packages/designer/src/project/project.ts +++ b/packages/designer/src/project/project.ts @@ -1,7 +1,8 @@ import { EventEmitter } from 'events'; -import { ProjectSchema, RootSchema, obx, computed } from '@ali/lowcode-globals'; +import { obx, computed } from '@ali/lowcode-editor-core'; import { Designer } from '../designer'; import { DocumentModel, isDocumentModel } from '../document'; +import { ProjectSchema, RootSchema } from '@ali/lowcode-types'; export class Project { private emitter = new EventEmitter(); diff --git a/packages/designer/src/simulator.ts b/packages/designer/src/simulator.ts index 2fa325f4a..9fe2af78a 100644 --- a/packages/designer/src/simulator.ts +++ b/packages/designer/src/simulator.ts @@ -1,5 +1,5 @@ import { Component as ReactComponent, ComponentType } from 'react'; -import { ComponentMetadata } from '@ali/lowcode-globals'; +import { ComponentMetadata } from '@ali/lowcode-types'; import { ISensor, Point, ScrollTarget, IScrollable } from './designer'; import { Node } from './document'; diff --git a/packages/editor-core/CHANGELOG.md b/packages/editor-core/CHANGELOG.md deleted file mode 100644 index 1f2f83c47..000000000 --- a/packages/editor-core/CHANGELOG.md +++ /dev/null @@ -1,47 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - - -## [0.8.6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.5...@ali/lowcode-editor-core@0.8.6) (2020-04-16) - - - - -**Note:** Version bump only for package @ali/lowcode-editor-core - - -## [0.8.5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.4...@ali/lowcode-editor-core@0.8.5) (2020-04-15) - - -### Bug Fixes - -* editor ([ccd9162](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ccd9162)) - - - - - -## [0.8.4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.3...@ali/lowcode-editor-core@0.8.4) (2020-03-30) - - - - -**Note:** Version bump only for package @ali/lowcode-editor-core - - -## 0.8.3 (2020-03-30) - - - - -**Note:** Version bump only for package @ali/lowcode-editor-core - - -## 0.8.2 (2020-03-30) - - - - -**Note:** Version bump only for package @ali/lowcode-editor-core diff --git a/packages/editor-core/README.md b/packages/editor-core/README.md index 6e9e79f5b..30c3e7d7d 100644 --- a/packages/editor-core/README.md +++ b/packages/editor-core/README.md @@ -1,11 +1,3 @@ -# demo component +shared globals -t-s-demo - -intro component - -## API - -| 参数名 | 说明 | 必填 | 类型 | 默认值 | 备注 | -| ------ | ---- | ---- | ---- | ------ | ---- | -| | | | | | | + 发 CDN diff --git a/packages/editor-core/build.json b/packages/editor-core/build.json index bd5cf18dd..e791d5b6b 100644 --- a/packages/editor-core/build.json +++ b/packages/editor-core/build.json @@ -1,5 +1,9 @@ { "plugins": [ - "build-plugin-component" + "build-plugin-component", + "build-plugin-fusion", + ["build-plugin-moment-locales", { + "locales": ["zh-cn"] + }] ] } diff --git a/packages/globals/cloud-build.json b/packages/editor-core/cloud-build.json similarity index 100% rename from packages/globals/cloud-build.json rename to packages/editor-core/cloud-build.json diff --git a/packages/editor-core/package.json b/packages/editor-core/package.json index ff51b4271..c8fcce18c 100644 --- a/packages/editor-core/package.json +++ b/packages/editor-core/package.json @@ -1,45 +1,56 @@ { - "name": "@ali/lowcode-editor-core", - "version": "0.8.6", - "description": "alibaba lowcode editor core", + "name": "@ali/lowcode-globals", + "version": "0.9.3", + "description": "Globals api for Ali lowCode engine", + "license": "MIT", "main": "lib/index.js", "module": "es/index.js", - "stylePath": "style.js", "files": [ "lib", "es" ], "scripts": { "build": "build-scripts build --skip-demo", + "cloud-build": "build-scripts build --skip-demo --config cloud-build.json", "test": "ava", "test:snapshot": "ava --update-snapshots" }, - "keywords": [ - "lowcode", - "editor" - ], - "author": "xiayang.xy", + "ava": { + "compileEnhancements": false, + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ], + "snapshotDir": "test/fixtures/__snapshots__" + }, "dependencies": { - "@ali/lowcode-globals": "^0.9.3", - "@alifd/next": "1.x", + "@alifd/next": "^1.19.16", + "@recore/obx": "^1.0.8", + "@recore/obx-react": "^1.0.7", + "classnames": "^2.2.6", + "power-di": "^2.2.4", "debug": "^4.1.1", - "events": "^3.1.0", "intl-messageformat": "^8.3.1", "lodash": "^4.17.15", - "prop-types": "^15.5.8", - "react": "^16.8.0", "store": "^2.0.12", - "whatwg-fetch": "^3.0.0" + "react": "^16", + "react-dom": "^16.7.0" }, "devDependencies": { - "@alib/build-scripts": "^0.1.3", + "@alib/build-scripts": "^0.1.18", + "@types/classnames": "^2.2.7", + "@types/node": "^13.7.1", + "@types/react": "^16", + "@types/react-dom": "^16", + "@types/lodash": "^4.14.149", - "@types/react": "^16.9.13", - "@types/react-dom": "^16.9.4", "@types/store": "^2.0.2", - "build-plugin-component": "^0.2.10" + "build-plugin-component": "^0.2.11", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0" }, - "license": "MIT", "publishConfig": { "registry": "https://registry.npm.alibaba-inc.com" } diff --git a/packages/editor-core/src/areaManager.ts b/packages/editor-core/src/areaManager.ts deleted file mode 100644 index 6f162c5cc..000000000 --- a/packages/editor-core/src/areaManager.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { PluginConfig, PluginStatus, PluginClass, HOCPlugin } from './definitions'; -import Editor from './index'; -import { clone, deepEqual } from './utils'; - -export default class AreaManager { - private pluginStatus: PluginStatus; - - private config: PluginConfig[]; - - constructor(private editor: Editor, private name: string) { - this.config = (editor && editor.config && editor.config.plugins && editor.config.plugins[name]) || []; - this.pluginStatus = clone(editor.pluginStatus); - } - - 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; - - const isUpdate = list.some( - (item): boolean => !deepEqual(pluginStatus[item.pluginKey], this.pluginStatus[item.pluginKey]), - ); - if (!notUpdateStatus) { - this.pluginStatus = clone(pluginStatus); - } - return isUpdate; - } - - 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; - } - - getPlugin(pluginKey: string): HOCPlugin | void { - if (pluginKey) { - return this.editor && this.editor.plugins && this.editor.plugins[pluginKey]; - } - } - - getPluginConfig(pluginKey?: string): PluginConfig[] | PluginConfig | undefined { - if (pluginKey) { - return this.config.find(item => item.pluginKey === pluginKey); - } - return this.config; - } - - getPluginClass(pluginKey: string): PluginClass | void { - if (pluginKey) { - return this.editor && this.editor.components && this.editor.components[pluginKey]; - } - } - - getPluginStatus(pluginKey: string): PluginStatus | void { - if (pluginKey) { - return this.editor && this.editor.pluginStatus && this.editor.pluginStatus[pluginKey]; - } - } -} diff --git a/packages/editor-core/src/context.ts b/packages/editor-core/src/context.ts deleted file mode 100644 index 859b79dc4..000000000 --- a/packages/editor-core/src/context.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react'; - -const context = createContext({}); -export default context; diff --git a/packages/globals/src/di/index.ts b/packages/editor-core/src/di/index.ts similarity index 50% rename from packages/globals/src/di/index.ts rename to packages/editor-core/src/di/index.ts index fafbd578c..d60f74e53 100644 --- a/packages/globals/src/di/index.ts +++ b/packages/editor-core/src/di/index.ts @@ -1,4 +1,3 @@ export * from './setter'; -export * from './transducer'; export * from './ioc-context'; -export * from './editor'; +export * from './tip'; diff --git a/packages/globals/src/di/ioc-context.ts b/packages/editor-core/src/di/ioc-context.ts similarity index 100% rename from packages/globals/src/di/ioc-context.ts rename to packages/editor-core/src/di/ioc-context.ts diff --git a/packages/globals/src/di/setter.ts b/packages/editor-core/src/di/setter.ts similarity index 90% rename from packages/globals/src/di/setter.ts rename to packages/editor-core/src/di/setter.ts index fb162022f..b66bfc06c 100644 --- a/packages/globals/src/di/setter.ts +++ b/packages/editor-core/src/di/setter.ts @@ -1,7 +1,6 @@ import { ReactNode } from 'react'; -import { CustomView, isCustomView } from '../types/setter-config'; -import { createContent } from '../utils/create-content'; -import { TitleContent } from '../types'; +import { CustomView, isCustomView, TitleContent } from '@ali/lowcode-types'; +import { createContent } from '@ali/lowcode-utils'; export type RegisteredSetter = { component: CustomView; diff --git a/packages/editor-core/src/editor.ts b/packages/editor-core/src/editor.ts index 70f1263ad..aa7fe1083 100644 --- a/packages/editor-core/src/editor.ts +++ b/packages/editor-core/src/editor.ts @@ -1,31 +1,10 @@ import { EventEmitter } from 'events'; -import store from 'store'; -import { IocContext, RegisterOptions } from '@ali/lowcode-globals'; -import { - EditorConfig, - HooksConfig, - LocaleType, - PluginStatusSet, - Utils, - PluginClassSet, - PluginSet, -} from './definitions'; - -import pluginFactory from './pluginFactory'; - -import * as editorUtils from './utils'; - -const { registShortCuts, transformToPromise, unRegistShortCuts } = editorUtils; - -let instance: Editor; - +import { IEditor, EditorConfig, PluginClassSet } from '@ali/lowcode-types'; +import { IocContext, RegisterOptions } from './di'; +import { globalLocale } from './intl'; EventEmitter.defaultMaxListeners = 100; -export interface HooksFuncs { - [idx: number]: (msg: string, handler: (...args: []) => void) => void; -} - -export type KeyType = Function | symbol | string; +export type KeyType = Function | Symbol | string; export type ClassType = Function | (new (...args: any[]) => any); export interface GetOptions { forceNew?: boolean; @@ -41,26 +20,7 @@ export type GetReturnType = T extends undefined const NOT_FOUND = Symbol.for('not_found'); -export default class Editor extends EventEmitter { - static getInstance = (config: EditorConfig, components: PluginClassSet, utils?: Utils): Editor => { - if (!instance) { - instance = new Editor(config, components, utils); - } - return instance; - }; - - private _components?: PluginClassSet; - get components(): PluginClassSet { - if (!this._components) { - this._components = {}; - Object.keys(this.componentsMap).forEach((key) => { - (this._components as any)[key] = pluginFactory(this.componentsMap[key]); - }); - } - return this._components; - } - - readonly utils: Utils; +export class Editor extends EventEmitter implements IEditor { /** * Ioc Container */ @@ -68,18 +28,12 @@ export default class Editor extends EventEmitter { notFoundHandler: (type: KeyType) => NOT_FOUND, }); - pluginStatus?: PluginStatusSet; + get locale() { + return globalLocale.getLocale(); + } - plugins?: PluginSet; - - locale?: LocaleType; - - hooksFuncs?: HooksFuncs; - - constructor(readonly config: EditorConfig = {}, readonly componentsMap: PluginClassSet = {}, utils?: Utils) { + constructor(readonly config: EditorConfig = {}, readonly components: PluginClassSet = {}) { super(); - this.utils = { ...editorUtils, ...utils } as any; - instance = this; } get(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType | undefined { @@ -135,18 +89,15 @@ export default class Editor extends EventEmitter { } async init(): Promise { - const { hooks, shortCuts = [], lifeCycles } = this.config || {}; - this.locale = store.get('lowcode-editor-locale') || 'zh-CN'; - this.pluginStatus = this.initPluginStatus(); - this.initHooks(hooks || []); + const { shortCuts = [], lifeCycles } = this.config || {}; this.emit('editor.beforeInit'); const init = (lifeCycles && lifeCycles.init) || ((): void => {}); // 用户可以通过设置extensions.init自定义初始化流程; try { - await transformToPromise(init(this)); + // await transformToPromise(init(this)); // 注册快捷键 - registShortCuts(shortCuts, this); + // registShortCuts(shortCuts, this); this.emit('editor.afterInit'); return true; } catch (err) { @@ -156,9 +107,8 @@ export default class Editor extends EventEmitter { destroy(): void { try { - const { hooks = [], shortCuts = [], lifeCycles = {} } = this.config; - unRegistShortCuts(shortCuts); - this.destroyHooks(hooks); + const { shortCuts = [], lifeCycles = {} } = this.config; + // unRegistShortCuts(shortCuts); if (lifeCycles.destroy) { lifeCycles.destroy(this); } @@ -167,33 +117,6 @@ export default class Editor extends EventEmitter { } } - batchOn(events: string[], lisenter: (...args: any[]) => void): void { - if (!Array.isArray(events)) { - return; - } - events.forEach((event): void => { - this.on(event, lisenter); - }); - } - - batchOnce(events: string[], lisenter: (...args: any[]) => void): void { - if (!Array.isArray(events)) { - return; - } - events.forEach((event): void => { - this.once(event, lisenter); - }); - } - - batchOff(events: string[], lisenter: (...args: any[]) => void): void { - if (!Array.isArray(events)) { - return; - } - events.forEach((event): void => { - this.off(event, lisenter); - }); - } - private waits = new Map< KeyType, Array<{ @@ -245,50 +168,4 @@ export default class Editor extends EventEmitter { this.waits.delete(key); } } - - // 销毁hooks中的消息监听 - private destroyHooks(hooks: HooksConfig = []): void { - hooks.forEach((item, idx): void => { - if (typeof this.hooksFuncs?.[idx] === 'function') { - this.off(item.message, this.hooksFuncs[idx]); - } - }); - delete this.hooksFuncs; - } - - // 初始化hooks中的消息监听 - private initHooks(hooks: HooksConfig = []): void { - this.hooksFuncs = hooks.map((item): ((...arg: any[]) => void) => { - const func = (...args: any[]): void => { - item.handler(this, ...args); - }; - this[item.type](item.message, func); - return func; - }); - } - - private initPluginStatus(): PluginStatusSet { - const { plugins = {} } = this.config; - const pluginAreas = Object.keys(plugins); - const res: PluginStatusSet = {}; - pluginAreas.forEach((area): void => { - (plugins[area] || []).forEach((plugin): void => { - if (plugin.type === 'Divider') { - return; - } - const { visible, disabled, marked } = plugin.props || {}; - res[plugin.pluginKey] = { - visible: typeof visible === 'boolean' ? visible : true, - disabled: typeof disabled === 'boolean' ? disabled : false, - marked: typeof marked === 'boolean' ? marked : false, - }; - const pluginClass = this.components[plugin.pluginKey]; - // 判断如果编辑器插件有init静态方法,则在此执行init方法 - if (pluginClass && pluginClass.init) { - pluginClass.init(this); - } - }); - }); - return res; - } } diff --git a/packages/globals/src/utils/hotkey.ts b/packages/editor-core/src/hotkey.ts similarity index 100% rename from packages/globals/src/utils/hotkey.ts rename to packages/editor-core/src/hotkey.ts diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index 89ecdbfda..f8d71e139 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -1,11 +1,6 @@ -import Editor from './editor'; - -import * as utils from './utils'; - -export { default as PluginFactory } from './pluginFactory'; -export { default as EditorContext } from './context'; -export { default as AreaManager } from './areaManager'; - -export default Editor; - -export { Editor, utils }; +export * from './intl'; +export * from './editor'; +export * from './utils'; +export * from './di'; +export * from './hotkey'; +export * from './widgets'; diff --git a/packages/globals/src/intl/ali-global-locale.ts b/packages/editor-core/src/intl/ali-global-locale.ts similarity index 97% rename from packages/globals/src/intl/ali-global-locale.ts rename to packages/editor-core/src/intl/ali-global-locale.ts index 9737a68ba..2a26727dd 100644 --- a/packages/globals/src/intl/ali-global-locale.ts +++ b/packages/editor-core/src/intl/ali-global-locale.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'events'; +import { obx } from '../utils/obx'; const languageMap: { [key: string]: string } = { en: 'en-US', zh: 'zh-CN', @@ -28,7 +29,7 @@ const languageMap: { [key: string]: string } = { const LowcodeConfigKey = 'ali-lowcode-config'; class AliGlobalLocale { - private locale: string = ''; + @obx.ref private locale: string = ''; private emitter = new EventEmitter(); constructor() { diff --git a/packages/globals/src/intl/index.tsx b/packages/editor-core/src/intl/index.ts similarity index 51% rename from packages/globals/src/intl/index.tsx rename to packages/editor-core/src/intl/index.ts index 2b7619d5c..3deb3e8b4 100644 --- a/packages/globals/src/intl/index.tsx +++ b/packages/editor-core/src/intl/index.ts @@ -1,19 +1,9 @@ +import { ReactNode, Component, createElement } from 'react'; +import { IntlMessageFormat } from 'intl-messageformat'; import { globalLocale } from './ali-global-locale'; -import { PureComponent, ReactNode } from 'react'; -import { isI18nData } from '../types'; +import { isI18nData } from '@ali/lowcode-types'; +import { observer, computed } from '../utils'; -function injectVars(template: string, params: any): string { - if (!template || !params) { - return template; - } - return template.replace(/({\w+})/g, (_, $1) => { - const key = (/\d+/.exec($1) || [])[0] as any; - if (key && params[key] != null) { - return params[key]; - } - return $1; - }); -} function generateTryLocales(locale: string) { const tries = [locale, locale.replace('-', '_')]; if (locale === 'zh-TW' || locale === 'en-US') { @@ -30,44 +20,40 @@ function generateTryLocales(locale: string) { return tries; } -export function localeFormat(data: any, params?: object): string { +function injectVars(msg: string, params: any, locale: string): string { + if (!msg || !params) { + return msg; + } + const formater = new IntlMessageFormat(msg, locale); + return formater.format(params as any) as string; + /* + + return template.replace(/({\w+})/g, (_, $1) => { + const key = (/\d+/.exec($1) || [])[0] as any; + if (key && params[key] != null) { + return params[key]; + } + return $1; + });*/ +} + +export function intl(data: any, params?: object): string { if (!isI18nData(data)) { return data; } const locale = globalLocale.getLocale(); const tries = generateTryLocales(locale); - let tpl: string | undefined; + let msg: string | undefined; for (const lan of tries) { - tpl = data[lan]; - if (tpl != null) { + msg = data[lan]; + if (msg != null) { break; } } - if (tpl == null) { + if (msg == null) { return `##intl@${locale}##`; } - return injectVars(tpl, params); -} - -class Intl extends PureComponent<{ data: any; params?: object }> { - private dispose = globalLocale.onLocaleChange(() => this.forceUpdate()); - componentWillUnmount() { - this.dispose(); - } - render() { - const { data, params } = this.props; - return localeFormat(data, params); - } -} - -export function intl(data: any, params?: object): ReactNode { - if (isI18nData(data)) { - if (data.intl) { - return data.intl; - } - return ; - } - return data; + return injectVars(msg, params, locale); } export function shallowIntl(data: any): any { @@ -76,38 +62,53 @@ export function shallowIntl(data: any): any { } const maps: any = {}; Object.keys(data).forEach(key => { - maps[key] = localeFormat(data[key]); + maps[key] = intl(data[key]); }); return maps; } +export function intlNode(data: any, params?: object): ReactNode { + if (isI18nData(data)) { + if (data.intlNode) { + return data.intlNode; + } + + return createElement(IntlElement, { data, params }); + } + return data; +} + +@observer +class IntlElement extends Component<{ data: any; params?: object }> { + render() { + const { data, params } = this.props; + return intl(data, params); + } +} + export function createIntl( instance: string | object, ): { - intl(id: string, params?: object): ReactNode; - intlString(id: string, params?: object): string; + intlNode(id: string, params?: object): ReactNode; + intl(id: string, params?: object): string; getLocale(): string; setLocale(locale: string): void; } { - let lastLocale: string | undefined; - let data: any = {}; - function useLocale(locale: string) { - lastLocale = locale; + const data = computed(() => { + const locale = globalLocale.getLocale(); if (typeof instance === 'string') { if ((window as any)[instance]) { - data = (window as any)[instance][locale] || {}; + data.messages = (window as any)[instance][locale] || {}; } else { const key = `${instance}_${locale.toLocaleLowerCase()}`; - data = (window as any)[key] || {}; + data.messages = (window as any)[key] || {}; } } else if (instance && typeof instance === 'object') { - data = (instance as any)[locale] || {}; + data.messages = (instance as any)[locale] || {}; } - } + }); - useLocale(globalLocale.getLocale()); - - function intlString(key: string, params?: object): string { + function intl(key: string, params?: object): string { // TODO: tries lost language const str = data[key]; @@ -115,30 +116,22 @@ export function createIntl( return `##intl@${key}##`; } - return injectVars(str, params); + return injectVars(str, params, globalLocale.getLocale()); } - class Intl extends PureComponent<{ id: string; params?: object }> { - private dispose = globalLocale.onLocaleChange(locale => { - if (lastLocale !== locale) { - useLocale(locale); - this.forceUpdate(); - } - }); - componentWillUnmount() { - this.dispose(); - } + @observer + class IntlElement extends Component<{ id: string; params?: object }> { render() { const { id, params } = this.props; - return intlString(id, params); + return intl(id, params); } } return { - intl(id: string, params?: object) { - return ; + intlNode(id: string, params?: object) { + return createElement(IntlElement, { id, params }); }, - intlString, + intl, getLocale() { return globalLocale.getLocale(); }, diff --git a/packages/editor-core/src/pluginFactory.tsx b/packages/editor-core/src/pluginFactory.tsx deleted file mode 100644 index 2eff60e9e..000000000 --- a/packages/editor-core/src/pluginFactory.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { createRef, PureComponent } from 'react'; - -import EditorContext from './context'; -import { I18nFunction, PluginProps, PluginClass, Plugin } from './definitions'; -import Editor from './editor'; -import { acceptsRef, generateI18n, isEmpty, transformToPromise } from './utils'; - -export default function pluginFactory(Comp: PluginClass): React.ComponentType { - class LowcodePlugin extends PureComponent { - public static displayName = 'LowcodeEditorPlugin'; - - public static contextType = EditorContext; - - public static init = Comp.init; - - public ref: React.RefObject & Plugin; - - private editor: Editor; - - private pluginKey: string; - - private i18n: I18nFunction; - - constructor(props, context) { - super(props, context); - if (isEmpty(props.config) || !props.config.pluginKey) { - console.warn('lowcode editor plugin has wrong config'); - return; - } - const { editor } = props; - this.ref = createRef(); - // 注册插件 - this.editor = editor; - this.pluginKey = props.config.pluginKey; - const defaultProps = Comp.defaultProps || {}; - const locale = this.editor.get('locale') || defaultProps.locale || 'zh-CN'; - const editorMessages = this.editor.get('messages') || {}; - const messages = editorMessages[this.pluginKey] || defaultProps.messages || {}; - this.i18n = generateI18n(locale, messages); - - editor.set('plugins', { - ...editor.plugins, - [this.pluginKey]: this, - }); - } - - public componentWillUnmount(): void { - // 销毁插件 - if (this.pluginKey && this.editor && this.editor.plugins) { - delete this.editor.plugins[this.pluginKey]; - } - } - - public open = (): Promise => { - if (this.ref && this.ref.open && typeof this.ref.open === 'function') { - return transformToPromise(this.ref.open()); - } - return Promise.resolve(); - }; - - public close = (): Promise => { - if (this.ref && this.ref.close && typeof this.ref.close === 'function') { - return transformToPromise(this.ref.close()); - } - return Promise.resolve(); - }; - - public render(): React.ReactNode { - const { config } = this.props; - const props = { - i18n: this.i18n, - editor: this.editor, - config, - ...config.pluginProps, - }; - if (acceptsRef(Comp)) { - props.ref = this.ref; - } - return ; - } - } - - return LowcodePlugin; -} diff --git a/packages/editor-core/src/utils.ts b/packages/editor-core/src/utils.ts deleted file mode 100644 index dd6d6d251..000000000 --- a/packages/editor-core/src/utils.ts +++ /dev/null @@ -1,273 +0,0 @@ -import IntlMessageFormat from 'intl-messageformat'; -import keymaster from 'keymaster'; - -import _clone from 'lodash/cloneDeep'; -import _debounce from 'lodash/debounce'; -import _isEmpty from 'lodash/isEmpty'; -import _deepEqual from 'lodash/isEqualWith'; -import _pick from 'lodash/pick'; -import _throttle from 'lodash/throttle'; - -import _serialize from 'serialize-javascript'; -export { get, post, request } from './request'; -import Editor from './editor'; -import { EditorConfig, I18nFunction, I18nMessages, LocaleType, ShortCutsConfig } from './definitions'; - -export const pick = _pick; -export const deepEqual = _deepEqual; -export const clone = _clone; -export const isEmpty = _isEmpty; -export const throttle = _throttle; -export const debounce = _debounce; - -export const serialize = _serialize; - -const ENV = { - TBE: 'TBE', - WEBIDE: 'WEB-IDE', - VSCODE: 'VSCODE', - WEB: 'WEB', -}; - -declare global { - interface Window { - sendIDEMessage?: (params: IDEMessageParams) => void; - goldlog?: { - record: (logKey: string, gmKey: string, goKey: string, method: 'POST' | 'GET') => (...args: any[]) => any; - }; - is_theia?: boolean; - vscode?: boolean; - } -} - -export interface IDEMessageParams { - action: string; - data: { - logKey: string; - gmKey: string; - goKey: string; - }; -} - -/* - * 用于构造国际化字符串处理函数 - */ -export function generateI18n(locale: LocaleType = 'zh-CN', messages: I18nMessages = {}): I18nFunction { - return (key: string, values): string => { - if (!messages || !messages[key]) { - return ''; - } - const formater = new IntlMessageFormat(messages[key], locale); - return formater.format(values); - }; -} - -/** - * 序列化参数 - */ -export function serializeParams(obj: object): string { - if (typeof obj !== 'object') { - return ''; - } - const res: string[] = []; - Object.entries(obj).forEach(([key, val]): void => { - if (val === null || val === undefined || val === '') { - return; - } - if (typeof val === 'object') { - res.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(val))}`); - } else { - res.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`); - } - }); - return res.join('&'); -} - -/** - * 黄金令箭埋点 - * @param {String} gmKey 为黄金令箭业务类型 - * @param {Object} params 参数 - * @param {String} logKey 属性串 - */ -export function goldlog(gmKey: string, params: object = {}, logKey: string = 'other'): void { - const sendIDEMessage = window.sendIDEMessage || window.parent.sendIDEMessage; - const goKey = serializeParams({ - env: getEnv(), - ...params, - }); - if (sendIDEMessage) { - sendIDEMessage({ - action: 'goldlog', - data: { - logKey: `/iceluna.core.${logKey}`, - gmKey, - goKey, - }, - }); - } - if (window.goldlog) { - window.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST'); - } -} - -/** - * 获取当前编辑器环境 - */ -export function getEnv(): string { - const userAgent = navigator.userAgent; - const isVscode = /Electron\//.test(userAgent); - if (isVscode) { - return ENV.VSCODE; - } - const isTheia = window.is_theia === true; - if (isTheia) { - return ENV.WEBIDE; - } - return ENV.WEB; -} - -// 注册快捷键 -export function registShortCuts(config: ShortCutsConfig, editor: Editor): void { - (config || []).forEach((item): void => { - keymaster(item.keyboard, (ev: Event): void => { - ev.preventDefault(); - item.handler(editor, ev, keymaster); - }); - }); -} - -// 取消注册快捷 -export function unRegistShortCuts(config: ShortCutsConfig): void { - (config || []).forEach((item): void => { - keymaster.unbind(item.keyboard); - }); - if (window.parent.vscode) { - keymaster.unbind('command+c'); - keymaster.unbind('command+v'); - } -} - -/** - * 将函数返回结果转成promise形式,如果函数有返回值则根据返回值的bool类型判断是reject还是resolve,若函数无返回值默认执行resolve - */ -export function transformToPromise(input: any): Promise<{}> { - if (input instanceof Promise) { - return input; - } - return new Promise((resolve, reject): void => { - if (input || input === undefined) { - resolve(); - } else { - reject(); - } - }); -} - -/** - * 将数组类型转换为Map类型 - */ -interface MapOf { - [propName: string]: T; -} -export function transformArrayToMap(arr: T[], key: string, overwrite: boolean = true): MapOf { - if (isEmpty(arr) || !Array.isArray(arr)) { - return {}; - } - const res = {}; - arr.forEach((item): void => { - const curKey = item[key]; - if (item[key] === undefined) { - return; - } - if (res[curKey] && !overwrite) { - return; - } - res[curKey] = item; - }); - return res; -} - -/** - * 解析url的查询参数 - */ -interface Query { - [propName: string]: string; -} -export function parseSearch(search: string): Query { - if (!search || typeof search !== 'string') { - return {}; - } - const str = search.replace(/^\?/, ''); - const paramStr = str.split('&'); - const res = {}; - paramStr.forEach((item): void => { - const regRes = item.split('='); - if (regRes[0] && regRes[1]) { - res[regRes[0]] = decodeURIComponent(regRes[1]); - } - }); - return res; -} - -export function comboEditorConfig(defaultConfig: EditorConfig = {}, customConfig: EditorConfig): EditorConfig { - const { skeleton, theme, plugins, hooks, shortCuts, lifeCycles, constants, utils, i18n } = customConfig || {}; - - if (skeleton && skeleton.handler && typeof skeleton.handler === 'function') { - return skeleton.handler({ - skeleton, - ...defaultConfig, - }); - } - - const defaultShortCuts = transformArrayToMap(defaultConfig.shortCuts || [], 'keyboard'); - const customShortCuts = transformArrayToMap(shortCuts || [], 'keyboard'); - const localeList = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP']; - const i18nConfig = {}; - localeList.forEach((key): void => { - i18nConfig[key] = { - ...(defaultConfig.i18n && defaultConfig.i18n[key]), - ...(i18n && i18n[key]), - }; - }); - return { - skeleton, - theme: { - ...defaultConfig.theme, - ...theme, - }, - plugins: { - ...defaultConfig.plugins, - ...plugins, - }, - hooks: [...(defaultConfig.hooks || []), ...(hooks || [])], - shortCuts: Object.values({ - ...defaultShortCuts, - ...customShortCuts, - }), - lifeCycles: { - ...defaultConfig.lifeCycles, - ...lifeCycles, - }, - constants: { - ...defaultConfig.constants, - ...constants, - }, - utils: [...(defaultConfig.utils || []), ...(utils || [])], - i18n: i18nConfig, - }; -} - -/** - * 判断当前组件是否能够设置ref - * @param {*} Comp 需要判断的组件 - */ -export function acceptsRef(Comp: React.ReactNode): boolean { - const hasSymbol = typeof Symbol === 'function' && Symbol.for; - const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0; - if (!Comp || typeof Comp !== 'object' || isEmpty(Comp)) { - return false; - } - return ( - (Comp.$$typeof && Comp.$$typeof === REACT_FORWARD_REF_TYPE) || (Comp.prototype && Comp.prototype.isReactComponent) - ); -} diff --git a/packages/editor-core/src/app-preset.ts b/packages/editor-core/src/utils/app-preset.ts similarity index 100% rename from packages/editor-core/src/app-preset.ts rename to packages/editor-core/src/utils/app-preset.ts diff --git a/packages/globals/src/utils/get-public-path.ts b/packages/editor-core/src/utils/get-public-path.ts similarity index 100% rename from packages/globals/src/utils/get-public-path.ts rename to packages/editor-core/src/utils/get-public-path.ts diff --git a/packages/editor-core/src/utils/goldlog.ts b/packages/editor-core/src/utils/goldlog.ts new file mode 100644 index 000000000..4e594b15c --- /dev/null +++ b/packages/editor-core/src/utils/goldlog.ts @@ -0,0 +1,11 @@ +/** + * 黄金令箭埋点 + * @param {String} gmKey 为黄金令箭业务类型 + * @param {Object} params 参数 + * @param {String} logKey 属性串 + */ +export function goldlog(gmKey: string, params: object = {}, logKey: string = 'other'): void { + +} + + diff --git a/packages/editor-core/src/utils/index.ts b/packages/editor-core/src/utils/index.ts new file mode 100644 index 000000000..8e2f1ccfa --- /dev/null +++ b/packages/editor-core/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from './get-public-path'; +export * from './goldlog'; +export * from './obx'; +export * from './request'; diff --git a/packages/globals/src/obx/index.ts b/packages/editor-core/src/utils/obx.ts similarity index 100% rename from packages/globals/src/obx/index.ts rename to packages/editor-core/src/utils/obx.ts diff --git a/packages/editor-core/src/request.ts b/packages/editor-core/src/utils/request.ts similarity index 91% rename from packages/editor-core/src/request.ts rename to packages/editor-core/src/utils/request.ts index 85eafabcf..24cde7293 100644 --- a/packages/editor-core/src/request.ts +++ b/packages/editor-core/src/utils/request.ts @@ -1,8 +1,10 @@ -import 'whatwg-fetch'; import Debug from 'debug'; const debug = Debug('request'); -export function serialize(obj: object): string { +export function serialize(obj?: object): string { + if (!obj) { + return ''; + } const rst: string[] = []; Object.entries(obj || {}).forEach(([key, val]): void => { if (val === null || val === undefined || val === '') return; @@ -12,7 +14,7 @@ export function serialize(obj: object): string { return rst.join('&'); } -export function buildUrl(dataAPI: string, params: object): string { +export function buildUrl(dataAPI: string, params?: object): string { const paramStr = serialize(params); if (paramStr) { return dataAPI.indexOf('?') > 0 ? `${dataAPI}&${paramStr}` : `${dataAPI}?${paramStr}`; @@ -25,7 +27,7 @@ export function get(dataAPI: string, params?: object, headers?: object, otherPro Accept: 'application/json', ...headers, }; - return request(buildUrl(dataAPI, params), 'GET', null, fetchHeaders, otherProps); + return request(buildUrl(dataAPI, params), 'GET', undefined, fetchHeaders, otherProps); } export function post(dataAPI: string, params?: object, headers?: object, otherProps?: object): Promise { @@ -50,7 +52,7 @@ export function request( method: string = 'GET', data?: object | string, headers?: object, - otherProps?: object, + otherProps?: any, ): Promise { return new Promise((resolve, reject): void => { if (otherProps && otherProps.timeout) { @@ -109,7 +111,7 @@ export function request( return null; } }) - .then((json: object): void => { + .then((json: any): void => { if (json && json.__success !== false) { resolve(json); } else { diff --git a/packages/globals/src/components/index.ts b/packages/editor-core/src/widgets/index.ts similarity index 63% rename from packages/globals/src/components/index.ts rename to packages/editor-core/src/widgets/index.ts index 196dee25a..d3260bcaa 100644 --- a/packages/globals/src/components/index.ts +++ b/packages/editor-core/src/widgets/index.ts @@ -1,3 +1,3 @@ +// TODO move another place export * from './tip'; export * from './title'; -export * from './svg-icon'; diff --git a/packages/editor-core/src/widgets/tip/index.ts b/packages/editor-core/src/widgets/tip/index.ts new file mode 100644 index 000000000..dba4412c7 --- /dev/null +++ b/packages/editor-core/src/widgets/tip/index.ts @@ -0,0 +1,4 @@ +import './style.less'; + +export * from './tip'; +export * from './tip-container'; diff --git a/packages/globals/src/components/tip/style.less b/packages/editor-core/src/widgets/tip/style.less similarity index 100% rename from packages/globals/src/components/tip/style.less rename to packages/editor-core/src/widgets/tip/style.less diff --git a/packages/globals/src/components/tip/tip-container.tsx b/packages/editor-core/src/widgets/tip/tip-container.tsx similarity index 83% rename from packages/globals/src/components/tip/tip-container.tsx rename to packages/editor-core/src/widgets/tip/tip-container.tsx index 31585c547..35ff58f14 100644 --- a/packages/globals/src/components/tip/tip-container.tsx +++ b/packages/editor-core/src/widgets/tip/tip-container.tsx @@ -1,8 +1,8 @@ import { Component } from 'react'; -import Tip from './tip'; -import tipHandler from './tip-handler'; +import { TipItem } from './tip-item'; +import { tipHandler } from './tip-handler'; -export default class TipContainer extends Component { +export class TipContainer extends Component { shouldComponentUpdate() { return false; } @@ -29,7 +29,7 @@ export default class TipContainer extends Component { render() { return (
- +
); } diff --git a/packages/globals/src/components/tip/tip-handler.ts b/packages/editor-core/src/widgets/tip/tip-handler.ts similarity index 93% rename from packages/globals/src/components/tip/tip-handler.ts rename to packages/editor-core/src/widgets/tip/tip-handler.ts index db347f847..d528a52af 100644 --- a/packages/globals/src/components/tip/tip-handler.ts +++ b/packages/editor-core/src/widgets/tip/tip-handler.ts @@ -1,54 +1,10 @@ import { EventEmitter } from 'events'; -import { TipConfig } from '../../types'; +import { TipConfig } from '@ali/lowcode-types'; export interface TipOptions extends TipConfig { target: HTMLElement; } -function findTip(target: HTMLElement | null): TipOptions | null { - if (!target) { - return null; - } - // optimize deep finding on mouseover - let loopupLimit = 10; - while (target && loopupLimit-- > 0) { - // get tip from target node - if (target.dataset && target.dataset.tip) { - return { - children: target.dataset.tip, - direction: (target.dataset.direction || target.dataset.dir) as any, - theme: target.dataset.theme, - target, - }; - } - - // or get tip from child nodes - let child: HTMLElement | null = target.lastElementChild as HTMLElement; - - while (child) { - if (child.dataset && child.dataset.role === 'tip') { - const tipId = child.dataset.tipId; - if (!tipId) { - return null; - } - const tipProps = tipsMap.get(tipId); - if (!tipProps) { - return null; - } - return { - ...tipProps, - target, - }; - } - child = child.previousElementSibling as HTMLElement; - } - - target = target.parentNode as HTMLElement; - } - - return null; -} - class TipHandler { tip: TipOptions | null = null; private showDelay: number | null = null; @@ -60,7 +16,7 @@ class TipHandler { if (tip) { if (this.tip) { // the some target should return - if (this.tip.target === tip.target) { + if ((this.tip as any).target === (tip as any).target) { this.tip = tip; return; } @@ -127,13 +83,57 @@ class TipHandler { } } +export const tipHandler = new TipHandler(); + +function findTip(target: HTMLElement | null): TipOptions | null { + if (!target) { + return null; + } + // optimize deep finding on mouseover + let loopupLimit = 10; + while (target && loopupLimit-- > 0) { + // get tip from target node + if (target.dataset && target.dataset.tip) { + return { + children: target.dataset.tip, + direction: (target.dataset.direction || target.dataset.dir) as any, + theme: target.dataset.theme, + target, + }; + } + + // or get tip from child nodes + let child: HTMLElement | null = target.lastElementChild as HTMLElement; + + while (child) { + if (child.dataset && child.dataset.role === 'tip') { + const tipId = child.dataset.tipId; + if (!tipId) { + return null; + } + const tipProps = tipsMap.get(tipId); + if (!tipProps) { + return null; + } + return { + ...tipProps, + target, + }; + } + child = child.previousElementSibling as HTMLElement; + } + + target = target.parentNode as HTMLElement; + } + + return null; +} + const tipsMap = new Map(); -export function saveTips(id: string, props: TipConfig | null) { +export function postTip(id: string, props: TipConfig | null) { if (props) { tipsMap.set(id, props); } else { tipsMap.delete(id); } } - -export default new TipHandler(); diff --git a/packages/globals/src/components/tip/tip.tsx b/packages/editor-core/src/widgets/tip/tip-item.tsx similarity index 91% rename from packages/globals/src/components/tip/tip.tsx rename to packages/editor-core/src/widgets/tip/tip-item.tsx index 20d6c7928..558d13768 100644 --- a/packages/globals/src/components/tip/tip.tsx +++ b/packages/editor-core/src/widgets/tip/tip-item.tsx @@ -1,10 +1,11 @@ import { Component } from 'react'; import classNames from 'classnames'; +import { intl } from '@ali/lowcode-editor-core'; +import { TipConfig } from '@ali/lowcode-types'; import { resolvePosition } from './utils'; -import tipHandler, { TipOptions } from './tip-handler'; -import { intl } from '../../intl'; +import { tipHandler } from './tip-handler'; -export default class Tip extends Component { +export class TipItem extends Component { private dispose?: () => void; constructor(props: any) { super(props); @@ -101,7 +102,7 @@ export default class Tip extends Component { } render() { - const tip: TipOptions = tipHandler.tip || ({} as any); + const tip: TipConfig = tipHandler.tip || ({} as any); const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null); this.originClassName = className; diff --git a/packages/editor-core/src/widgets/tip/tip.tsx b/packages/editor-core/src/widgets/tip/tip.tsx new file mode 100644 index 000000000..4da9bdfc9 --- /dev/null +++ b/packages/editor-core/src/widgets/tip/tip.tsx @@ -0,0 +1,17 @@ +import { Component } from 'react'; +import { TipConfig } from '@ali/lowcode-types'; +import { uniqueId } from '@ali/lowcode-utils'; +import { postTip } from './tip-handler'; + +export class Tip extends Component { + private id = uniqueId('tips$'); + + componentWillUnmount() { + postTip(this.id, null); + } + + render() { + postTip(this.id, this.props); + return ; + } +} diff --git a/packages/globals/src/components/tip/utils.ts b/packages/editor-core/src/widgets/tip/utils.ts similarity index 100% rename from packages/globals/src/components/tip/utils.ts rename to packages/editor-core/src/widgets/tip/utils.ts diff --git a/packages/globals/src/components/title/index.tsx b/packages/editor-core/src/widgets/title/index.tsx similarity index 80% rename from packages/globals/src/components/title/index.tsx rename to packages/editor-core/src/widgets/title/index.tsx index 247ec517b..4b19bda72 100644 --- a/packages/globals/src/components/title/index.tsx +++ b/packages/editor-core/src/widgets/title/index.tsx @@ -1,10 +1,10 @@ import { Component, isValidElement } from 'react'; import classNames from 'classnames'; -import EmbedTip from '../tip/embed-tip'; -import './title.less'; -import { createIcon } from '../../utils'; -import { TitleContent, isI18nData } from '../../types'; +import { createIcon } from '@ali/lowcode-utils'; +import { TitleContent, isI18nData } from '@ali/lowcode-types'; import { intl } from '../../intl'; +import { Tip } from '../tip'; +import './title.less'; export class Title extends Component<{ title: TitleContent; className?: string; onClick?: () => void }> { render() { @@ -20,14 +20,14 @@ export class Title extends Component<{ title: TitleContent; className?: string; let tip: any = null; if (title.tip) { - if (isValidElement(title.tip) && title.tip.type === EmbedTip) { + if (isValidElement(title.tip) && title.tip.type === Tip) { tip = title.tip; } else { const tipProps = typeof title.tip === 'object' && !(isValidElement(title.tip) || isI18nData(title.tip)) ? title.tip : { children: title.tip }; - tip = ; + tip = ; } } diff --git a/packages/globals/src/components/title/title.less b/packages/editor-core/src/widgets/title/title.less similarity index 100% rename from packages/globals/src/components/title/title.less rename to packages/editor-core/src/widgets/title/title.less diff --git a/packages/editor-core/tsconfig.json b/packages/editor-core/tsconfig.json index c37b76ecc..91c180bdd 100644 --- a/packages/editor-core/tsconfig.json +++ b/packages/editor-core/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "outDir": "lib" }, - "include": [ - "./src/" - ] + "include": ["./src/"], } diff --git a/packages/editor-skeleton/src/area.ts b/packages/editor-skeleton/src/area.ts new file mode 100644 index 000000000..aafbab857 --- /dev/null +++ b/packages/editor-skeleton/src/area.ts @@ -0,0 +1,59 @@ +import { obx, computed } from '@ali/lowcode-editor-core'; +import WidgetContainer from './widget/widget-container'; +import { Skeleton } from './skeleton'; +import { IWidget } from './widget/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; + } + + get current() { + if (this.exclusive) { + return this.container.current; + } + return null; + } + + 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 = current; + this.container.unactive(current); + } + return; + } + this._visible = flag; + } + + hide() { + this.setVisible(false); + } + + show() { + this.setVisible(true); + } +} diff --git a/packages/editor-skeleton/src/components/LeftPlugin/index.scss b/packages/editor-skeleton/src/components/LeftPlugin/index.scss deleted file mode 100644 index f78fca1ee..000000000 --- a/packages/editor-skeleton/src/components/LeftPlugin/index.scss +++ /dev/null @@ -1,59 +0,0 @@ -.lowcode-left-plugin { - font-size: 20px; - text-align: center; - line-height: 44px; - height: 44px; - position: relative; - cursor: pointer; - transition: all 0.3s ease; - color: $color-text1-3; - &.locked { - color: red !important; - } - &:hover { - color: $color-brand1-6; - &:before { - content: attr(data-tooltip); - display: block; - position: absolute; - left: 45px; - top: 8px; - line-height: 18px; - font-size: 12px; - white-space: nowrap; - padding: 6px 8px; - border-radius: 4px; - background: $balloon-normal-color-bg; - border: 1px solid $balloon-normal-color-border; - color: $color-text1-3; - z-index: 100; - } - &:after { - content: ''; - display: block; - position: absolute; - width: 10px; - height: 10px; - transform: rotate(45deg); - left: 40px; - top: 18px; - background: $balloon-normal-color-bg; - border-left: 1px solid $balloon-normal-color-border; - border-bottom: 1px solid $balloon-normal-color-border; - z-index: 100; - } - } - &.active { - color: $color-brand1-9; - &.disabled { - color: $color-text1-1; - } - &:hover { - color: $color-brand1-6; - } - } - &.disabled { - cursor: not-allowed; - color: $color-text1-1; - } -} diff --git a/packages/editor-skeleton/src/components/LeftPlugin/index.tsx b/packages/editor-skeleton/src/components/LeftPlugin/index.tsx deleted file mode 100644 index 7770c942c..000000000 --- a/packages/editor-skeleton/src/components/LeftPlugin/index.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React, { PureComponent, Fragment } from 'react'; -import classNames from 'classnames'; -import { Balloon, Dialog, Icon, Badge } from '@alifd/next'; -import Editor from '@ali/lowcode-editor-core'; -import { - PluginConfig, - PluginClass, -} from '@ali/lowcode-editor-core/lib/definitions'; -import './index.scss'; - -export interface LeftPluginProps { - active?: boolean; - config: PluginConfig; - disabled?: boolean; - editor: Editor; - locked?: boolean; - marked?: boolean; - onClick?: () => void; - pluginClass: PluginClass | undefined; -} - -export interface LeftPluginState { - dialogVisible: boolean; -} - -export default class LeftPlugin extends PureComponent< - LeftPluginProps, - LeftPluginState -> { - static displayName = 'LowcodeLeftPlugin'; - - static defaultProps = { - active: false, - config: {}, - disabled: false, - marked: false, - locked: false, - onClick: (): void => {}, - }; - - constructor(props, context) { - super(props, context); - this.state = { - dialogVisible: false, - }; - } - - componentDidMount(): void { - const { config, editor } = this.props; - const pluginKey = config && config.pluginKey; - if (editor && pluginKey) { - editor.on(`${pluginKey}.dialog.show`, this.handleShow); - editor.on(`${pluginKey}.dialog.close`, this.handleClose); - } - } - - componentWillUnmount(): void { - const { config, editor } = this.props; - const pluginKey = config && config.pluginKey; - if (editor && pluginKey) { - editor.off(`${pluginKey}.dialog.show`, this.handleShow); - editor.off(`${pluginKey}.dialog.close`, this.handleClose); - } - } - - handleClose = (): void => { - const { config, editor } = this.props; - const pluginKey = config && config.pluginKey; - const plugin = editor.plugins && editor.plugins[pluginKey]; - if (plugin) { - plugin.close().then((): void => { - this.setState({ - dialogVisible: false, - }); - }); - } - }; - - handleOpen = (): void => { - // todo 对话框类型的插件初始时拿不到插件实例 - this.setState({ - dialogVisible: true, - }); - }; - - handleShow = (): void => { - const { disabled, config, onClick, editor } = this.props; - const pluginKey = config && config.pluginKey; - if (disabled || !pluginKey) return; - this.handleOpen(); - // 考虑到弹窗情况,延时发送消息 - setTimeout((): void => { - editor.emit(`${pluginKey}.plugin.activate`); - }, 0); - if (onClick) { - onClick(); - } - }; - - renderIcon = (clickCallback): React.ReactNode => { - const { active, disabled, marked, locked, onClick, config } = this.props; - const { pluginKey, props } = config || {}; - const { icon, title } = props || {}; - return ( -
{ - if (disabled) return; - // 考虑到弹窗情况,延时发送消息 - clickCallback && clickCallback(); - - onClick && onClick(); - }} - > - {marked ? ( - - - - ) : ( - - )} -
- ); - }; - - render(): React.ReactNode { - const { - marked, - locked, - active, - disabled, - config, - editor, - pluginClass: Comp, - } = this.props; - const { pluginKey, props, type, pluginProps } = config || {}; - const { onClick, title } = props || {}; - const { dialogVisible } = this.state; - if (!pluginKey || !type || !props) return null; - - const node = Comp ? ( - { - onClick && onClick.call(null, editor); - }} - {...pluginProps} - /> - ) : null; - switch (type) { - case 'LinkIcon': - return ( - - {this.renderIcon((): void => { - onClick && onClick.call(null, editor); - })} - - ); - case 'Icon': - return this.renderIcon((): void => { - onClick && onClick.call(null, editor); - }); - case 'DialogIcon': - return ( - - {this.renderIcon((): void => { - onClick && onClick.call(null, editor); - this.handleOpen(); - })} - { - editor.emit(`${pluginKey}.dialog.onOk`); - this.handleClose(); - }} - onCancel={this.handleClose} - onClose={this.handleClose} - title={title} - style={{ - width: 500, - ...(props.dialogProps && props.dialogProps.style), - }} - {...(props.dialogProps || {})} - visible={dialogVisible} - > - {node} - - - ); - case 'BalloonIcon': - return ( - { - onClick && onClick.call(null, editor); - })} - align="r" - triggerType={['click', 'hover']} - {...(props.balloonProps || {})} - > - {node} - - ); - case 'PanelIcon': - return this.renderIcon((): void => { - onClick && onClick.call(null, editor); - }); - case 'Custom': - return marked ? {node} : node; - default: - return null; - } - } -} diff --git a/packages/editor-skeleton/src/components/Panel/index.scss b/packages/editor-skeleton/src/components/Panel/index.scss deleted file mode 100644 index 8a9660638..000000000 --- a/packages/editor-skeleton/src/components/Panel/index.scss +++ /dev/null @@ -1,52 +0,0 @@ -.lowcode-panel { - user-select: none; - overflow: hidden; - position: relative; - background: $card-background; - transition: width 0.3s ease; - transform: translate3d(0, 0, 0); - height: 100%; - &.visible { - border-right: 1px solid $color-line1-1; - } - .drag-area { - display: none; - } - &.floatable { - position: absolute; - top: 0; - bottom: 0; - z-index: 999; - } - &.draggable { - .drag-area { - display: block; - width: 10px; - position: absolute; - top: 0; - bottom: 0; - cursor: col-resize; - z-index: 9999; - } - &.left { - .drag-area { - right: 0; - } - } - &.right { - .drag-area { - left: 0; - } - } - } - &.left { - &.floatable { - left: 50px; - } - } - &.right { - &.floatable { - right: 48px; - } - } -} diff --git a/packages/editor-skeleton/src/components/Panel/index.tsx b/packages/editor-skeleton/src/components/Panel/index.tsx deleted file mode 100644 index 6cdb1bd40..000000000 --- a/packages/editor-skeleton/src/components/Panel/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { PureComponent } from 'react'; -import classNames from 'classnames'; - -import './index.scss'; - -export interface PanelProps { - align: 'left' | 'right'; - defaultWidth: number; - minWidth: number; - draggable: boolean; - floatable: boolean; - children: Plugin; - visible: boolean; -} - -export interface PanelState { - width: number; -} - -export default class Panel extends PureComponent { - static displayName = 'LowcodePanel'; - - static defaultProps = { - align: 'left', - defaultWidth: 240, - minWidth: 100, - draggable: true, - floatable: false, - visible: true, - }; - - constructor(props) { - super(props); - - this.state = { - width: props.defaultWidth, - }; - } - - render(): React.ReactNode { - const { align, draggable, floatable, visible } = this.props; - const { width } = this.state; - return ( -
- {this.props.children} -
-
- ); - } -} diff --git a/packages/editor-skeleton/src/components/TopIcon/index.scss b/packages/editor-skeleton/src/components/TopIcon/index.scss deleted file mode 100644 index 065fb859e..000000000 --- a/packages/editor-skeleton/src/components/TopIcon/index.scss +++ /dev/null @@ -1,77 +0,0 @@ -.lowcode-top-icon { - display: inline-block; - width: 44px; - font-size: 20px; - line-height: 48px; - color: $color-text1-3; - position: relative; - &.disabled { - cursor: not-allowed; - color: $color-text1-1; - &:hover { - color: $color-text1-1; - } - } - &.active { - color: $color-brand1-9; - &:hover { - color: $color-brand1-6; - } - } - &.locked { - color: red !important; - } - &:hover { - color: $color-brand1-6; - &:before { - content: attr(data-tooltip); - display: block; - height: auto; - width: auto; - position: absolute; - left: 50%; - transform: translate(-50%, 0); - bottom: -32px; - line-height: 18px; - font-size: 12px; - white-space: nowrap; - padding: 6px 8px; - border-radius: 4px; - background: $balloon-normal-color-bg; - border: 1px solid $balloon-normal-color-border; - color: $color-text1-3; - z-index: 100; - } - &:after { - content: ''; - display: block; - position: absolute; - width: 10px; - height: 10px; - left: 50%; - transform: translate(-50%, 0) rotate(45deg); - bottom: -5px; - background: $balloon-normal-color-bg; - border-left: 1px solid $balloon-normal-color-border; - border-top: 1px solid $balloon-normal-color-border; - opacity: 1; - visibility: visible; - z-index: 100; - } - } - i.next-icon { - &:before { - font-size: 16px; - } - margin-right: 0; - line-height: 18px; - } - span { - display: block; - margin: 0px -5px 0; - line-height: 16px; - text-align: center; - font-size: 12px; - transform: scale(0.8); - } -} diff --git a/packages/editor-skeleton/src/components/TopIcon/index.tsx b/packages/editor-skeleton/src/components/TopIcon/index.tsx deleted file mode 100644 index 939f77ffc..000000000 --- a/packages/editor-skeleton/src/components/TopIcon/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { PureComponent } from 'react'; -import classNames from 'classnames'; -import { Icon } from '@alifd/next'; - -import './index.scss'; - -export interface TopIconProps { - active?: boolean; - className?: string; - disabled?: boolean; - icon: string; - id?: string; - locked?: boolean; - marked?: boolean; - onClick?: () => void; - style?: React.CSSProperties; - title?: string; -} - -export default class TopIcon extends PureComponent { - static displayName = 'LowcodeTopIcon'; - - static defaultProps = { - active: false, - className: '', - disabled: false, - icon: '', - id: '', - locked: false, - onClick: (): void => {}, - style: {}, - title: '', - }; - - render(): React.ReactNode { - const { - active, - disabled, - icon, - locked, - title, - className, - id, - style, - onClick, - } = this.props; - return ( -
- -
- ); - } -} diff --git a/packages/editor-skeleton/src/components/TopPlugin/index.scss b/packages/editor-skeleton/src/components/TopPlugin/index.scss deleted file mode 100644 index 4bdd7b8d2..000000000 --- a/packages/editor-skeleton/src/components/TopPlugin/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -.lowcode-top-addon { -} diff --git a/packages/editor-skeleton/src/components/TopPlugin/index.tsx b/packages/editor-skeleton/src/components/TopPlugin/index.tsx deleted file mode 100644 index 71eccf28a..000000000 --- a/packages/editor-skeleton/src/components/TopPlugin/index.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React, { PureComponent, Fragment } from 'react'; - -import { Balloon, Badge, Dialog } from '@alifd/next'; -import Editor from '@ali/lowcode-editor-core'; -import { - PluginConfig, - PluginClass, -} from '@ali/lowcode-editor-core/lib/definitions'; -import TopIcon from '../TopIcon'; - -import './index.scss'; - -export interface TopPluginProps { - active?: boolean; - config: PluginConfig; - disabled?: boolean; - editor: Editor; - locked?: boolean; - marked?: boolean; - onClick?: () => void; - pluginClass: PluginClass | undefined; -} - -export interface TopPluginState { - dialogVisible: boolean; -} - -export default class TopPlugin extends PureComponent< - TopPluginProps, - TopPluginState -> { - static displayName = 'LowcodeTopPlugin'; - - static defaultProps = { - active: false, - config: {}, - disabled: false, - marked: false, - locked: false, - onClick: (): void => {}, - }; - - constructor(props, context) { - super(props, context); - this.state = { - dialogVisible: false, - }; - } - - componentDidMount(): void { - const { config, editor } = this.props; - const pluginKey = config && config.pluginKey; - if (editor && pluginKey) { - editor.on(`${pluginKey}.dialog.show`, this.handleShow); - editor.on(`${pluginKey}.dialog.close`, this.handleClose); - } - } - - componentWillUnmount(): void { - const { config, editor } = this.props; - const pluginKey = config && config.pluginKey; - if (editor && pluginKey) { - editor.off(`${pluginKey}.dialog.show`, this.handleShow); - editor.off(`${pluginKey}.dialog.close`, this.handleClose); - } - } - - handleShow = (): void => { - const { disabled, config, onClick, editor } = this.props; - const pluginKey = config && config.pluginKey; - if (disabled || !pluginKey) return; - this.handleOpen(); - // 考虑到弹窗情况,延时发送消息 - setTimeout((): void => { - editor.emit(`${pluginKey}.plugin.activate`); - }, 0); - onClick && onClick(); - }; - - handleClose = (): void => { - const { config, editor } = this.props; - const pluginKey = config && config.pluginKey; - const plugin = editor.plugins && editor.plugins[pluginKey]; - if (plugin) { - plugin.close().then((): void => { - this.setState({ - dialogVisible: false, - }); - }); - } - }; - - handleOpen = (): void => { - // todo dialog类型的插件初始时拿不动插件实例 - this.setState({ - dialogVisible: true, - }); - }; - - renderIcon = (clickCallback): React.ReactNode => { - const { - active, - disabled, - marked, - locked, - config, - onClick, - editor, - } = this.props; - const { pluginKey, props } = config || {}; - const { icon, title } = props || {}; - const node = ( - { - if (disabled) return; - // 考虑到弹窗情况,延时发送消息 - setTimeout((): void => { - editor.emit(`${pluginKey}.plugin.activate`); - }, 0); - clickCallback && clickCallback(); - onClick && onClick(); - }} - /> - ); - return marked ? {node} : node; - }; - - render(): React.ReactNode { - const { - active, - marked, - locked, - disabled, - config, - editor, - pluginClass: Comp, - } = this.props; - const { pluginKey, pluginProps, props, type } = config || {}; - const { onClick, title } = props || {}; - const { dialogVisible } = this.state; - if (!pluginKey || !type) return null; - const node = Comp ? ( - { - onClick && onClick.call(null, editor); - }} - {...pluginProps} - /> - ) : null; - - switch (type) { - case 'LinkIcon': - return ( - - {this.renderIcon((): void => { - onClick && onClick.call(null, editor); - })} - - ); - case 'Icon': - return this.renderIcon((): void => { - onClick && onClick.call(null, editor); - }); - case 'DialogIcon': - return ( - - {this.renderIcon((): void => { - onClick && onClick.call(null, editor); - this.handleOpen(); - })} - { - editor.emit(`${pluginKey}.dialog.onOk`); - this.handleClose(); - }} - onCancel={this.handleClose} - onClose={this.handleClose} - title={title} - style={{ - width: 500, - ...(props.dialogProps && props.dialogProps.style), - }} - {...props.dialogProps} - visible={dialogVisible} - > - {node} - - - ); - case 'BalloonIcon': - return ( - { - onClick && onClick.call(null, editor); - })} - triggerType={['click', 'hover']} - {...props.balloonProps} - > - {node} - - ); - case 'Custom': - return marked ? {node} : node; - default: - return null; - } - } -} diff --git a/packages/editor-skeleton/src/components/array-setter/bugs.md b/packages/editor-skeleton/src/components/array-setter/bugs.md new file mode 100644 index 000000000..8fe96cc16 --- /dev/null +++ b/packages/editor-skeleton/src/components/array-setter/bugs.md @@ -0,0 +1,5 @@ +* 拖拽排序有问题 +* forceInline 有问题 +* 部分改变不响应 +* 样式还原 +* autofocus diff --git a/packages/editor-skeleton/src/components/array-setter/index.tsx b/packages/editor-skeleton/src/components/array-setter/index.tsx new file mode 100644 index 000000000..2f12b6e8f --- /dev/null +++ b/packages/editor-skeleton/src/components/array-setter/index.tsx @@ -0,0 +1,280 @@ +import { Component, Fragment } from 'react'; +import { Icon, Button, Message } from '@alifd/next'; +import { Title } from '@ali/lowcode-editor-core'; +import { SetterType, FieldConfig, SetterConfig } from '@ali/lowcode-types'; +import { SettingField } from '@ali/lowcode-designer'; +import { createSettingFieldView } from '../settings/settings-pane'; +import { PopupContext, PopupPipe } from '../popup'; +import Sortable from './sortable'; +import './style.less'; + +interface ArraySetterState { + items: SettingField[]; + itemsMap: Map; + prevLength: number; +} + +interface ArraySetterProps { + value: any[]; + field: SettingField; + itemSetter?: SetterType; + columns?: FieldConfig[]; + multiValue?: boolean; +} + +export class ListSetter extends Component { + static getDerivedStateFromProps(props: ArraySetterProps, state: ArraySetterState) { + const { value, field } = props; + const newLength = value && Array.isArray(value) ? value.length : 0; + if (state && state.prevLength === newLength) { + return null; + } + + // props value length change will go here + const originLength = state ? state.items.length : 0; + if (state && originLength === newLength) { + return { + prevLength: newLength, + }; + } + + const itemsMap = state ? state.itemsMap : new Map(); + let items = state ? state.items.slice() : []; + if (newLength > originLength) { + for (let i = originLength; i < newLength; i++) { + const item = field.createField({ + name: i, + setter: props.itemSetter, + // FIXME: + forceInline: 1, + }); + items[i] = item; + itemsMap.set(item.id, item); + } + } else if (newLength < originLength) { + const deletes = items.splice(newLength); + deletes.forEach((item) => { + itemsMap.delete(item.id); + }); + } + return { + items, + itemsMap, + prevLength: newLength, + }; + } + + state: ArraySetterState = { + items: [], + itemsMap: new Map(), + prevLength: 0, + }; + + onSort(sortedIds: Array) { + const { itemsMap } = this.state; + const items = sortedIds.map((id, index) => { + const item = itemsMap.get(id)!; + item.setKey(index); + return item; + }); + this.setState({ + items, + }); + } + + private scrollToLast: boolean = false; + onAdd() { + const { items, itemsMap } = this.state; + const { itemSetter } = this.props; + const initialValue = typeof itemSetter === 'object' ? (itemSetter as any).initialValue : null; + const item = this.props.field.createField({ + name: items.length, + setter: itemSetter, + // FIXME: + forceInline: 1, + }); + items.push(item); + itemsMap.set(item.id, item); + item.setValue(typeof initialValue === 'function' ? initialValue(item) : initialValue); + this.scrollToLast = true; + this.setState({ + items: items.slice(), + }); + } + + onRemove(field: SettingField) { + const { items } = this.state; + let i = items.indexOf(field); + if (i < 0) { + return; + } + items.splice(i, 1); + const l = items.length; + while (i < l) { + items[i].setKey(i); + i++; + } + field.remove(); + this.setState({ items: items.slice() }); + } + + componentWillUnmount() { + this.state.items.forEach((field) => { + field.purge(); + }); + } + + shouldComponentUpdate(_: any, nextState: ArraySetterState) { + if (nextState.items !== this.state.items) { + return true; + } + return false; + } + + render() { + let columns: any = null; + if (this.props.columns) { + columns = this.props.columns.map((column) => ); + } + + const { items } = this.state; + const scrollToLast = this.scrollToLast; + this.scrollToLast = false; + const lastIndex = items.length - 1; + + const content = + items.length > 0 ? ( + <div className="lc-setter-list-scroll-body"> + <Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}> + {items.map((field, index) => ( + <ArrayItem + key={field.id} + scrollIntoView={scrollToLast && index === lastIndex} + field={field} + onRemove={this.onRemove.bind(this, field)} + /> + ))} + </Sortable> + </div> + ) : this.props.multiValue ? ( + <Message type="warning">当前选择了多个节点,且值不一致,修改会覆盖所有值</Message> + ) : ( + <Message type="notice">当前项目为空</Message> + ); + + return ( + <div className="lc-setter-list lc-block-setter"> + {/*<div className="lc-block-setter-actions"> + <Button size="medium" onClick={this.onAdd.bind(this)}> + <Icon type="add" /> + <span>添加</span> + </Button> + </div>*/} + {columns && <div className="lc-setter-list-columns">{columns}</div>} + {content} + <Button className="lc-setter-list-add" type="primary" onClick={this.onAdd.bind(this)}> + <Icon type="add" /> + <span>添加一项</span> + </Button> + </div> + ); + } +} + +class ArrayItem extends Component<{ + field: SettingField; + onRemove: () => void; + scrollIntoView: boolean; +}> { + shouldComponentUpdate() { + return false; + } + private shell?: HTMLDivElement | null; + componentDidMount() { + if (this.props.scrollIntoView && this.shell) { + this.shell.parentElement!.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + render() { + const { onRemove, field } = this.props; + return ( + <div className="lc-listitem" ref={(ref) => (this.shell = ref)}> + <div draggable className="lc-listitem-handler"> + <Icon type="ellipsis" size="small" /> + </div> + <div className="lc-listitem-body">{createSettingFieldView(field, field.parent)}</div> + <div className="lc-listitem-actions"> + <div className="lc-listitem-action" onClick={onRemove}> + <Icon type="ashbin" size="small" /> + </div> + </div> + </div> + ); + } +} + +class TableSetter extends ListSetter { + // todo: + // forceInline = 1 + // has more actions +} + +export default class ArraySetter extends Component<{ + value: any[]; + field: SettingField; + itemSetter?: SetterType; + mode?: 'popup' | 'list'; + forceInline?: boolean; + multiValue?: boolean; +}> { + static contextType = PopupContext; + private pipe: any; + render() { + const { mode, forceInline, ...props } = this.props; + const { field, itemSetter } = props; + let columns: FieldConfig[] | undefined; + if ((itemSetter as SetterConfig)?.componentName === 'ObjectSetter') { + const items: FieldConfig[] = (itemSetter as any).props?.config?.items; + if (items && Array.isArray(items)) { + columns = items.filter((item) => item.isRequired || item.important || (item.setter as any)?.isRequired); + if (columns.length > 4) { + columns = columns.slice(0, 4); + } + } + } + + if (mode === 'popup' || forceInline) { + const title = ( + <Fragment> + 编辑: + <Title title={field.title} /> + </Fragment> + ); + if (!this.pipe) { + let width = 360; + if (columns) { + if (columns.length === 3) { + width = 480; + } else if (columns.length > 3) { + width = 600; + } + } + this.pipe = (this.context as PopupPipe).create({ width }); + } + this.pipe.send(<TableSetter key={field.id} {...props} columns={columns} />, title); + return ( + <Button + type={forceInline ? 'normal' : 'primary'} + onClick={(e) => { + this.pipe.show((e as any).target, field.id); + }} + > + <Icon type="edit" /> + {forceInline ? title : '编辑数组'} + </Button> + ); + } else { + return <ListSetter {...props} columns={columns?.slice(0, 2)} />; + } + } +} diff --git a/packages/editor-skeleton/src/components/array-setter/sortable.less b/packages/editor-skeleton/src/components/array-setter/sortable.less new file mode 100644 index 000000000..12e9512cd --- /dev/null +++ b/packages/editor-skeleton/src/components/array-setter/sortable.less @@ -0,0 +1,29 @@ +.lc-sortable { + position: relative; + + .lc-sortable-card { + box-sizing: border-box; + &:after, &:before { + content: ""; + display: table; + } + &:after { + clear: both; + } + + &.lc-dragging { + outline: 2px dashed var(--color-brand); + outline-offset: -2px; + > * { + visibility: hidden; + } + border-color: transparent !important; + box-shadow: none !important; + background: transparent !important; + } + } + + [draggable] { + cursor: ns-resize; + } +} diff --git a/packages/editor-skeleton/src/components/array-setter/sortable.tsx b/packages/editor-skeleton/src/components/array-setter/sortable.tsx new file mode 100644 index 000000000..3329470f1 --- /dev/null +++ b/packages/editor-skeleton/src/components/array-setter/sortable.tsx @@ -0,0 +1,220 @@ +import { Component, Children, ReactElement } from 'react'; +import classNames from 'classnames'; +import './sortable.less'; + +class Sortable extends Component<{ + className?: string; + itemClassName?: string; + onSort?: (sortedIds: Array<string | number>) => void; + dragImageSourceHandler?: (elem: Element) => Element; + children: ReactElement[]; +}> { + private shell?: HTMLDivElement | null; + private items?: Array<string | number>; + private willDetach?: () => void; + componentDidMount() { + const box = this.shell!; + + let isDragEnd: boolean = false; + + /** + * target node to be dragged + */ + let source: Element | null; + + /** + * node to be placed + */ + let ref: Element | null; + + /** + * next sibling of the source node + */ + let origRef: Element | null; + + /** + * accurately locate the node from event + */ + const locate = (e: DragEvent) => { + let y = e.clientY; + if (e.view !== window && e.view!.frameElement) { + y += e.view!.frameElement.getBoundingClientRect().top; + } + let node = box.firstElementChild as HTMLDivElement; + while (node) { + if (node !== source && node.dataset.id) { + const rect = node.getBoundingClientRect(); + + if (rect.height <= 0) continue; + if (y < rect.top + rect.height / 2) { + break; + } + } + node = node.nextElementSibling as HTMLDivElement; + } + return node; + }; + + /** + * find the source node + */ + const getSource = (e: DragEvent) => { + const target = e.target as Element; + if (!target || !box.contains(target) || target === box) { + return null; + } + + let node = box.firstElementChild; + while (node) { + if (node.contains(target)) { + return node; + } + node = node.nextElementSibling; + } + + return null; + }; + + const sort = (beforeId: string | number | null | undefined) => { + if (!source) return; + + const sourceId = (source as HTMLDivElement).dataset.id; + const items = this.items!; + const origIndex = items.findIndex(id => id == sourceId); + + let newIndex = beforeId ? items.findIndex(id => id == beforeId) : items.length; + + if (origIndex < 0 || newIndex < 0) return; + if (this.props.onSort) { + if (newIndex > origIndex) { + newIndex -= 1; + } + if (origIndex === newIndex) return; + const item = items.splice(origIndex, 1); + items.splice(newIndex, 0, item[0]); + + this.props.onSort(items); + } + }; + + const dragstart = (e: DragEvent) => { + isDragEnd = false; + source = getSource(e); + if (!source) { + return false; + } + origRef = source.nextElementSibling; + const rect = source.getBoundingClientRect(); + let dragSource = source; + if (this.props.dragImageSourceHandler) { + dragSource = this.props.dragImageSourceHandler(source); + } + if (e.dataTransfer) { + e.dataTransfer.setDragImage(dragSource, e.clientX - rect.left, e.clientY - rect.top); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.dropEffect = 'move'; + try { + e.dataTransfer.setData('application/json', {} as any); + } catch (ex) { + // eslint-disable-line + } + } + + setTimeout(() => { + source!.classList.add('lc-dragging'); + }, 0); + return true; + }; + + const placeAt = (beforeRef: Element | null) => { + if (beforeRef) { + if (beforeRef !== source) { + box.insertBefore(source!, beforeRef); + } + } else { + box.appendChild(source!); + } + }; + + const adjust = (e: DragEvent) => { + if (isDragEnd) return; + ref = locate(e); + placeAt(ref); + }; + + let lastDragEvent: DragEvent | null; + const drag = (e: DragEvent) => { + if (!source) return; + e.preventDefault(); + if (lastDragEvent) { + if (lastDragEvent.clientX === e.clientX && lastDragEvent.clientY === e.clientY) { + return; + } + } + lastDragEvent = e; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + } + adjust(e); + }; + + const dragend = (e: DragEvent) => { + isDragEnd = true; + if (!source) return; + e.preventDefault(); + source.classList.remove('lc-dragging'); + placeAt(origRef); + sort(ref ? (ref as HTMLDivElement).dataset.id : null); + source = null; + ref = null; + origRef = null; + lastDragEvent = null; + }; + + box.addEventListener('dragstart', dragstart); + document.addEventListener('dragover', drag); + document.addEventListener('drag', drag); + document.addEventListener('dragend', dragend); + + this.willDetach = () => { + box.removeEventListener('dragstart', dragstart); + document.removeEventListener('dragover', drag); + document.removeEventListener('drag', drag); + document.removeEventListener('dragend', dragend); + }; + } + + componentWillUnmount() { + if (this.willDetach) { + this.willDetach(); + } + } + + render() { + const { className, itemClassName, children } = this.props; + const items: Array<string | number> = []; + const cards = Children.map(children, child => { + const id = child.key!; + items.push(id); + return ( + <div key={id} data-id={id} className={classNames('lc-sortable-card', itemClassName)}> + {child} + </div> + ); + }); + this.items = items; + + return ( + <div + className={classNames('lc-sortable', className)} + ref={ref => { + this.shell = ref; + }} + > + {cards} + </div> + ); + } +} + +export default Sortable; diff --git a/packages/editor-skeleton/src/components/array-setter/style.less b/packages/editor-skeleton/src/components/array-setter/style.less new file mode 100644 index 000000000..8e0da024b --- /dev/null +++ b/packages/editor-skeleton/src/components/array-setter/style.less @@ -0,0 +1,103 @@ +.lc-setter-list { + [draggable] { + cursor: move; + } + color: var(--color-text); + + .next-btn { + display: inline-flex; + align-items: center; + line-height: 1 !important; + max-width: 100%; + text-overflow: ellipsis; + } + + .lc-setter-list-add { + display: block; + width: 100%; + margin-top: 8px;; + } + + + .lc-setter-list-columns { + display: flex; + > .lc-title { + flex: 1; + justify-content: center; + } + margin-left: 47px; + margin-right: 28px; + margin-bottom: 5px; + } + + .lc-setter-list-scroll-body { + margin: -8px -5px; + padding: 8px 10px; + overflow-y: auto; + max-height: 300px; + } + + .lc-setter-list-card { + border: 1px solid rgba(31,56,88,.2); + background-color: var(--color-block-background-light); + border-radius: 3px; + &:not(:last-child) { + margin-bottom: 5px; + } + + .lc-listitem { + position: relative; + outline: none; + display: flex; + align-items: stretch; + height: 34px; + + .lc-listitem-actions { + margin: 0 3px; + display: inline-flex; + align-items: center; + justify-content: flex-end; + .lc-listitem-action { + text-align: center; + cursor: pointer; + opacity: 0.6; + &:hover { + opacity: 1; + } + } + } + .lc-listitem-body { + flex: 1; + display: flex; + align-items: stretch; + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + .lc-field { + padding: 0 !important; + display: flex; + align-items: center; + >.lc-field-body { + justify-content: center; + } + } + > * { + width: 100%; + } + .next-btn { + display: block; + width: 100%; + } + } + .lc-listitem-handler { + margin-left: 2px; + display: inline-flex; + align-items: center; + .next-icon-ellipsis { + transform: rotate(90deg); + } + opacity: 0.6; + } + } + } +} diff --git a/packages/editor-skeleton/src/components/field/fields.tsx b/packages/editor-skeleton/src/components/field/fields.tsx new file mode 100644 index 000000000..59d67df2f --- /dev/null +++ b/packages/editor-skeleton/src/components/field/fields.tsx @@ -0,0 +1,185 @@ +import { Component } from 'react'; +import classNames from 'classnames'; +import { Icon } from '@alifd/next'; +import { Title } from '@ali/lowcode-editor-core'; +import { TitleContent } from '@ali/lowcode-types'; +import { PopupPipe, PopupContext } from '../popup'; +import './index.less'; + +export interface FieldProps { + className?: string; + title?: TitleContent | null; + defaultDisplay?: 'accordion' | 'inline' | 'block'; + collapsed?: boolean; + onExpandChange?: (expandState: boolean) => void; +} + +export class Field extends Component<FieldProps> { + state = { + collapsed: this.props.collapsed, + display: this.props.defaultDisplay || 'inline', + }; + + private toggleExpand = () => { + const { onExpandChange } = this.props; + const collapsed = !this.state.collapsed; + this.setState({ + collapsed, + }); + onExpandChange && onExpandChange(!collapsed); + }; + private body: HTMLDivElement | null = null; + private dispose?: () => void; + private deployBlockTesting() { + if (this.dispose) { + this.dispose(); + } + const body = this.body; + if (!body) { + return; + } + const check = () => { + const setter = body.firstElementChild; + if (setter && setter.classList.contains('lc-block-setter')) { + this.setState({ + display: 'block', + }); + } else { + this.setState({ + display: 'inline', + }); + } + }; + const observer = new MutationObserver(check); + check(); + observer.observe(body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'], + }); + this.dispose = () => observer.disconnect(); + } + componentDidMount() { + const { defaultDisplay } = this.props; + if (!defaultDisplay || defaultDisplay === 'inline') { + this.deployBlockTesting(); + } + } + componentWillUnmount() { + if (this.dispose) { + this.dispose(); + } + } + + render() { + const { className, children, title } = this.props; + const { display, collapsed } = this.state; + const isAccordion = display === 'accordion'; + return ( + <div + className={classNames(`lc-field lc-${display}-field`, className, { + 'lc-field-is-collapsed': isAccordion && collapsed, + })} + > + <div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}> + <div className="lc-field-title"> + <Title title={title || ''} /> + </div> + {isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />} + </div> + <div key="body" ref={(shell) => (this.body = shell)} className="lc-field-body"> + {children} + </div> + </div> + ); + } +} + +export interface PopupFieldProps extends FieldProps { + width?: number; +} + +export class PopupField extends Component<PopupFieldProps> { + static contextType = PopupContext; + private pipe: any; + + static defaultProps: PopupFieldProps = { + width: 300, + }; + + render() { + const { className, children, title, width } = this.props; + if (!this.pipe) { + this.pipe = (this.context as PopupPipe).create({ width }); + } + + const titleElement = title && ( + <div className="lc-field-title"> + <Title title={title} /> + </div> + ); + + this.pipe.send(<div className="lc-field-body">{children}</div>, titleElement); + + return ( + <div className={classNames('lc-field lc-popup-field', className)}> + {title && ( + <div + className="lc-field-head" + onClick={(e) => { + this.pipe.show((e as any).target); + }} + > + <div className="lc-field-title"> + <Title title={title} /> + </div> + <Icon className="lc-field-icon" type="arrow-left" size="xs" /> + </div> + )} + </div> + ); + } +} + +export interface EntryFieldProps extends FieldProps { + stageName?: string; +} + +export class EntryField extends Component<EntryFieldProps> { + render() { + const { stageName, title, className } = this.props; + const classNameList = classNames('engine-setting-field', 'engine-entry-field', className); + const fieldProps: any = {}; + + if (stageName) { + // 为 stage 切换奠定基础 + fieldProps['data-stage-target'] = 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 PlainField extends Component<FieldProps> { + render() { + const { className, children } = this.props; + return ( + <div className={classNames(`lc-field lc-plain-field`, className)}> + <div className="lc-field-body">{children}</div> + </div> + ); + } +} diff --git a/packages/editor-skeleton/src/components/field/index.less b/packages/editor-skeleton/src/components/field/index.less new file mode 100644 index 000000000..5fecb74d9 --- /dev/null +++ b/packages/editor-skeleton/src/components/field/index.less @@ -0,0 +1,154 @@ +@import '../variables.less'; + +@x-gap: 10px; +@y-gap: 8px; + +.lc-field { + // head + .lc-field-head { + display: flex; + align-items: center; + justify-content: space-between; + + .lc-field-title { + display: flex; + align-items: center; + } + .lc-field-icon { + margin-right: @x-gap; + transform-origin: center; + transition: transform 0.1s; + } + } + + &.lc-plain-field { + // for top-level style + padding: 8px 10px; + > .lc-field-body { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + } + } + + &.lc-inline-field { + display: flex; + align-items: center; + // for top-level style + padding: 8px 10px; + + > .lc-field-head { + width: 70px; + margin-right: 1px; + .lc-title-label { + width: 70px; + word-break: break-all; + } + } + > .lc-field-body { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + } + } + + &.lc-block-field, &.lc-accordion-field { + display: block; + &:not(:first-child) { + border-top: 1px solid var(--color-line-normal, @dark-alpha-2); + } + > .lc-field-head { + padding-left: @x-gap; + height: 32px; + display: flex; + align-items: center; + font-weight: 500; + background: var(--color-block-background-shallow, rgba(31,56,88,.06)); + border-bottom: 1px solid var(--color-line-normal, @dark-alpha-2); + color: var(--color-title, @white-alpha-2); + user-select: none; + } + + > .lc-field-body { + padding: @y-gap @x-gap/2; + } + + + .lc-inline-field { + border-top: 1px solid var(--color-line-normal, @dark-alpha-2); + } + } + + .lc-setter-actions { + display: flex; + align-items: center; + } + + &.lc-block-field { + position: relative; + >.lc-field-body>.lc-block-setter>.lc-setter-actions { + position: absolute; + right: 10px; + top: 0; + height: 32px; + display: flex; + align-items: center; + } + } + + &.lc-accordion-field { + // collapsed + &.lc-field-is-collapsed { + > .lc-field-head .lc-field-icon { + transform: rotate(180deg); + } + > .lc-field-body { + display: none; + } + } + + // 邻近的保持上下距离 + + .lc-field { + margin-top: @y-gap; + } + } + + // 2rd level reset + .lc-field-body { + .lc-inline-field { + padding: @y-gap @x-gap/2 0 @x-gap/2; + &:first-child { + padding-top: 0; + } + + .lc-accordion-field, +.lc-block-field { + margin-top: @y-gap; + } + } + + .lc-field { + border-top: none !important; + } + + .lc-accordion-field, .lc-block-field { + > .lc-field-head { + padding-left: @x-gap/2; + background: var(--color-block-background-light); + border-bottom-color: var(--color-line-light); + > .lc-field-icon { + margin-right: @x-gap/2; + } + } + } + + // 3rd level field title width should short + .lc-field-body .lc-inline-field { + > .lc-field-head { + width: 50px; + .lc-title-label { + width: 50px; + } + } + } + } +} diff --git a/packages/editor-skeleton/src/components/field/index.ts b/packages/editor-skeleton/src/components/field/index.ts new file mode 100644 index 000000000..b0f3c497d --- /dev/null +++ b/packages/editor-skeleton/src/components/field/index.ts @@ -0,0 +1,28 @@ +import { ReactNode, createElement } from 'react'; +import { TitleContent } from '@ali/lowcode-types'; +import './index.less'; +import { Field, PopupField, EntryField, PlainField } from './fields'; + +export interface FieldProps { + className?: string; + title?: TitleContent | null; + display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; + collapsed?: boolean; + onExpandChange?: (collapsed: boolean) => void; + [extra: string]: any; +} + +export function createField(props: FieldProps, children: ReactNode, type?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry') { + if (type === 'popup') { + return createElement(PopupField, props, children); + } + if (type === 'entry') { + return createElement(EntryField, props, children); + } + if (type === 'plain' || !props.title) { + return createElement(PlainField, props, children); + } + return createElement(Field, { ...props, defaultDisplay: type }, children); +} + +export { Field, PopupField, EntryField, PlainField }; diff --git a/packages/editor-skeleton/src/components/mixed-setter/index.tsx b/packages/editor-skeleton/src/components/mixed-setter/index.tsx new file mode 100644 index 000000000..68530e9f8 --- /dev/null +++ b/packages/editor-skeleton/src/components/mixed-setter/index.tsx @@ -0,0 +1,262 @@ +import React, { Component, isValidElement } from 'react'; +import classNames from 'classnames'; +import { Dropdown, Menu } from '@alifd/next'; +import { + SetterConfig, + CustomView, + DynamicProps, + DynamicSetter, + TitleContent, + isSetterConfig, + isDynamicSetter, + isI18nData, +} from '@ali/lowcode-types'; +import { + getSetter, + getSettersMap, + computed, + obx, + Title, + createSetterContent, + observer, + shallowIntl, + Tip, +} from '@ali/lowcode-editor-core'; + +import { IconConvert } from '../../icons/convert'; + +import './style.less'; +import { SettingField } from '@ali/lowcode-designer'; + +export interface SetterItem { + name: string; + title: TitleContent; + setter: string | DynamicSetter | CustomView; + props?: object | DynamicProps; + condition?: (field: SettingField) => boolean; + initialValue?: any | ((field: SettingField) => any); + list: boolean; +} + +function nomalizeSetters(setters?: Array<string | SetterConfig | CustomView | DynamicSetter>): SetterItem[] { + if (!setters) { + const normalized: SetterItem[] = []; + getSettersMap().forEach((setter, name) => { + if (name === 'MixedSetter') { + return; + } + normalized.push({ + name, + title: setter.title || name, + setter: name, + condition: setter.condition, + initialValue: setter.initialValue, + list: setter.recommend || false, + }); + }); + return normalized; + } + const names: string[] = []; + function generateName(n: string) { + let idx = 1; + let got = n; + while (names.indexOf(got) > -1) { + got = `${n}:${idx++}`; + } + names.push(got); + return got; + } + return setters.map((setter) => { + const config: any = { + setter, + list: true, + }; + if (isSetterConfig(setter)) { + config.setter = setter.componentName; + config.props = setter.props; + config.condition = setter.condition; + config.initialValue = setter.initialValue; + config.title = setter.title; + } + if (typeof config.setter === 'string') { + config.name = config.setter; + names.push(config.name); + const info = getSetter(config.setter); + if (!config.title) { + config.title = info?.title || config.setter; + } + if (!config.condition) { + config.condition = info?.condition; + } + if (!config.initialValue) { + config.initialValue = info?.initialValue; + } + } else { + config.name = generateName((config.setter as any).displayName || (config.setter as any).name || 'CustomSetter'); + if (!config.title) { + config.title = config.name; + } + } + return config; + }); +} + +@observer +export default class MixedSetter extends Component<{ + field: SettingField; + setters?: Array<string | SetterConfig | CustomView | DynamicSetter>; + onSetterChange?: (field: SettingField, name: string) => void; + onChange?: (val: any) => void; + value?: any; + className?: string; +}> { + private setters = nomalizeSetters(this.props.setters); + @obx.ref private used?: string; + @computed private getCurrentSetter() { + const { field } = this.props; + let firstMatched: SetterItem | undefined; + for (const setter of this.setters) { + const matched = !setter.condition || setter.condition(field); + if (matched) { + if (setter.name === this.used) { + return setter; + } + if (!firstMatched) { + firstMatched = setter; + } + } + } + return firstMatched; + } + + private useSetter = (name: string) => { + if (name === this.used) { + return; + } + const { field, onChange } = this.props; + const setter = this.setters.find((item) => item.name === name); + this.used = name; + if (setter) { + let newValue: any = setter.initialValue; + if (newValue && typeof newValue === 'function') { + newValue = newValue(field); + } + onChange && onChange(newValue); + } + }; + + private shell: HTMLDivElement | null = null; + private checkIsBlockField() { + if (this.shell) { + const setter = this.shell.firstElementChild; + if (setter && setter.classList.contains('lc-block-setter')) { + this.shell.classList.add('lc-block-setter'); + } else { + this.shell.classList.remove('lc-block-setter'); + } + } + } + componentDidUpdate() { + this.checkIsBlockField(); + } + componentDidMount() { + this.checkIsBlockField(); + } + + render() { + const { className, field, setters, onSetterChange, ...restProps } = this.props; + + const currentSetter = this.getCurrentSetter(); + const isTwoType = this.setters.length < 3; + + let setterContent: any; + const triggerTitle: any = { + tip: { + type: 'i18n', + 'zh-CN': '切换格式', + 'en-US': 'Switch Format', + }, + icon: <IconConvert size={24} />, + }; + if (currentSetter) { + const { setter, title, props } = currentSetter; + let setterProps: any = {}; + let setterType: any; + if (isDynamicSetter(setter)) { + setterType = setter.call(field, field); + } else { + setterType = setter; + } + if (props) { + setterProps = props; + if (typeof setterProps === 'function') { + setterProps = setterProps(field); + } + } + + setterContent = createSetterContent(setterType, { + ...shallowIntl(setterProps), + field, + ...restProps, + }); + if (title) { + if (typeof title !== 'object' || isI18nData(title) || isValidElement(title)) { + triggerTitle.tip = title; + } else { + triggerTitle.tip = title.tip || title.label; + } + } + } else { + // 未匹配的 null 值,显示 NullValue 空值 + // 未匹配的 其它 值,显示 InvalidValue 非法值 + if (restProps.value == null) { + setterContent = <span>NullValue</span>; + } else { + setterContent = <span>InvalidValue</span>; + } + } + const usedName = currentSetter?.name || this.used; + let moreBtnNode = ( + <Title + title={triggerTitle} + className="lc-switch-trigger" + onClick={ + isTwoType + ? () => { + if (this.setters[0]?.name === usedName) { + this.useSetter(this.setters[1]?.name); + } else { + this.useSetter(this.setters[0]?.name); + } + } + : undefined + } + /> + ); + if (!isTwoType) { + moreBtnNode = ( + <Dropdown trigger={moreBtnNode} triggerType="click" align="tr br"> + <Menu selectMode="single" hasSelectedIcon={true} selectedKeys={usedName} onItemClick={this.useSetter}> + {this.setters + .filter((setter) => setter.list || setter.name === usedName) + .map((setter) => { + return ( + <Menu.Item key={setter.name}> + <Title title={setter.title} /> + </Menu.Item> + ); + })} + </Menu> + </Dropdown> + ); + } + + return ( + <div ref={(shell) => (this.shell = shell)} className={classNames('lc-setter-mixed', className)}> + {setterContent} + + <div className="lc-setter-actions">{moreBtnNode}</div> + </div> + ); + } +} diff --git a/packages/editor-skeleton/src/components/mixed-setter/style.less b/packages/editor-skeleton/src/components/mixed-setter/style.less new file mode 100644 index 000000000..6efebb28c --- /dev/null +++ b/packages/editor-skeleton/src/components/mixed-setter/style.less @@ -0,0 +1,30 @@ +.lc-setter-mixed { + flex: 1; + min-width: 0; + margin-right: 26px; + display: block; + position: relative; + >.lc-setter-actions { + position: absolute; + right: -2px; + top: 50%; + transform: translate(100%, -50%); + .lc-switch-trigger { + cursor: pointer; + opacity: 0.6; + &:hover { + opacity: 1; + } + } + } + .next-input,.next-date-picker { + width: 100%; + } + &.lc-block-setter { + position: static; + margin-right: 0; + >.lc-setter-actions { + transform: none; + } + } +} diff --git a/packages/editor-skeleton/src/components/object-setter/index.tsx b/packages/editor-skeleton/src/components/object-setter/index.tsx new file mode 100644 index 000000000..0d8d5e950 --- /dev/null +++ b/packages/editor-skeleton/src/components/object-setter/index.tsx @@ -0,0 +1,181 @@ +import { Component, Fragment } from 'react'; +import { Icon, Button } from '@alifd/next'; +import { SetterType, FieldConfig } from '@ali/lowcode-types'; +import { createSettingFieldView } from '../settings/settings-pane'; +import { PopupContext, PopupPipe } from '../popup'; +import { SettingField } from '@ali/lowcode-designer'; +import './style.less'; +import { Title } from '@ali/lowcode-editor-core'; + +export default class ObjectSetter extends Component<{ + field: SettingField; + descriptor?: string | ((rowField: SettingField) => string); + config: ObjectSetterConfig; + mode?: 'popup' | 'form'; + // 1: in tablerow 2: in listrow 3: in column-cell + forceInline?: number; +}> { + render() { + const { mode, forceInline = 0, ...props } = this.props; + if (forceInline || mode === 'popup') { + if (forceInline > 2 || mode === 'popup') { + // popup + return <RowSetter {...props} primaryButton={forceInline ? false : true} />; + } else { + return <RowSetter columns={forceInline > 1 ? 2 : 4} {...props} />; + } + } else { + // form + return <FormSetter {...props} />; + } + } +} + +interface ObjectSetterConfig { + items?: FieldConfig[]; + extraSetter?: SetterType; +} + +interface RowSetterProps { + field: SettingField; + descriptor?: string | ((rowField: SettingField) => string); + config: ObjectSetterConfig; + columns?: number; + primaryButton?: boolean; +} + +class RowSetter extends Component<RowSetterProps> { + static contextType = PopupContext; + + state: any = { + descriptor: '', + }; + + private items?: SettingField[]; + constructor(props: RowSetterProps) { + super(props); + const { config, descriptor, field, columns } = props; + const items: SettingField[] = []; + if (columns && config.items) { + const l = Math.min(config.items.length, columns); + for (let i = 0; i < l; i++) { + const conf = config.items[i]; + if (conf.isRequired || conf.important || (conf.setter as any)?.isRequired) { + const item = field.createField({ + ...conf, + // in column-cell + forceInline: 3, + }); + items.push(item); + } + } + } + + if (items.length > 0) { + this.items = items; + } + + let firstRun: boolean = true; + field.onEffect(() => { + let state: any = {}; + if (descriptor) { + if (typeof descriptor === 'function') { + state.descriptor = descriptor(field); + } else { + state.descriptor = field.getPropValue(descriptor); + } + } else { + state.descriptor = field.title; + } + + if (firstRun) { + firstRun = false; + this.state = state; + } else { + this.setState(state); + } + }); + } + + shouldComponentUpdate(_: any, nextState: any) { + if (this.state.decriptor !== nextState.decriptor) { + return true; + } + return false; + } + + private pipe: any; + render() { + const items = this.items; + const { field, primaryButton, config } = this.props; + + if (!this.pipe) { + this.pipe = (this.context as PopupPipe).create({ width: 320 }); + } + + const title = ( + <Fragment> + 编辑: + <Title title={this.state.descriptor} /> + </Fragment> + ); + + this.pipe.send(<FormSetter key={field.id} field={field} config={config} />, title); + + if (items) { + return ( + <div className="lc-setter-object-row"> + <div + className="lc-setter-object-row-edit" + onClick={(e) => { + this.pipe.show((e as any).target, field.id); + }} + > + <Icon size="small" type="edit" /> + </div> + <div className="lc-setter-object-row-body">{items.map((item) => createSettingFieldView(item, field))}</div> + </div> + ); + } + + return ( + <Button + type={primaryButton === false ? 'normal' : 'primary'} + onClick={(e) => { + this.pipe.show((e as any).target, field.id); + }} + > + <Icon type="edit" /> + {title} + </Button> + ); + } +} + +interface FormSetterProps { + field: SettingField; + config: ObjectSetterConfig; +} +class FormSetter extends Component<FormSetterProps> { + private items: SettingField[]; + constructor(props: RowSetterProps) { + super(props); + const { config, field } = props; + this.items = (config.items || []).map((conf) => field.createField(conf)); + + // TODO: extraConfig for custom fields + } + + shouldComponentUpdate() { + return false; + } + + render() { + const { field } = this.props; + return ( + <div className="lc-setter-object lc-block-setter"> + {this.items.map((item, index) => createSettingFieldView(item, field, index))} + </div> + ); + } +} diff --git a/packages/editor-skeleton/src/components/object-setter/style.less b/packages/editor-skeleton/src/components/object-setter/style.less new file mode 100644 index 000000000..6d6064df1 --- /dev/null +++ b/packages/editor-skeleton/src/components/object-setter/style.less @@ -0,0 +1,31 @@ +.lc-setter-object-row { + display: flex; + align-items: stretch; + width: 100%; + .lc-setter-object-row-edit { + width: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + .lc-setter-object-row-body { + display: flex; + flex: 1; + min-width: 0; + align-items: center; + .lc-field { + padding: 0 !important; + .lc-field-body { + padding: 0 !important; margin: 0 !important; + } + } + > * { + flex: 1; + flex-shrink: 1; + margin-left: 2px; + min-width: 0; + overflow: hidden; + } + } +} diff --git a/packages/editor-skeleton/src/components/popup/index.tsx b/packages/editor-skeleton/src/components/popup/index.tsx new file mode 100644 index 000000000..9018531cb --- /dev/null +++ b/packages/editor-skeleton/src/components/popup/index.tsx @@ -0,0 +1,150 @@ +import { createContext, ReactNode, Component, PureComponent } from 'react'; +import { EventEmitter } from 'events'; +import { Balloon } from '@alifd/next'; +import { uniqueId } from '@ali/lowcode-utils'; +import './style.less'; + +export const PopupContext = createContext<PopupPipe>({} as any); + +export class PopupPipe { + private emitter = new EventEmitter(); + private currentId?: string; + + create(props?: object): { send: (content: ReactNode, title: ReactNode) => void; show: (target: Element) => void } { + let sendContent: ReactNode = null; + let sendTitle: ReactNode = null; + const id = uniqueId('popup'); + return { + send: (content: ReactNode, title: ReactNode) => { + sendContent = content; + sendTitle = title; + if (this.currentId === id) { + this.popup({ + ...props, + content, + title, + }); + } + }, + show: (target: Element, actionKey?: string) => { + this.currentId = id; + this.popup( + { + ...props, + actionKey, + content: sendContent, + title: sendTitle, + }, + target, + ); + }, + }; + } + + private popup(props: object, target?: Element) { + Promise.resolve().then(() => { + this.emitter.emit('popupchange', props, target); + }); + } + + onPopupChange(fn: (props: object, target?: Element) => void): () => void { + this.emitter.on('popupchange', fn); + return () => { + this.emitter.removeListener('popupchange', fn); + }; + } + + purge() { + this.emitter.removeAllListeners(); + } +} + +export default class PopupService extends Component<{ actionKey?: string; safeId?: string }> { + private popupPipe = new PopupPipe(); + + componentWillUnmount() { + this.popupPipe.purge(); + } + + render() { + const { children, actionKey, safeId } = this.props; + return ( + <PopupContext.Provider value={this.popupPipe}> + {children} + <PopupContent key={'pop' + actionKey} safeId={safeId} /> + </PopupContext.Provider> + ); + } +} + +export class PopupContent extends PureComponent<{ safeId?: string }> { + static contextType = PopupContext; + state: any = { + visible: false, + pos: {}, + }; + + private dispose = (this.context as PopupPipe).onPopupChange((props, target) => { + const state: any = { + ...props, + visible: true, + }; + if (target) { + const rect = target.getBoundingClientRect(); + state.pos = { + top: rect.top, + height: rect.height, + }; + // todo: compute the align method + } + this.setState(state); + }); + + componentWillUnmount() { + this.dispose(); + } + + render() { + const { content, visible, width, title, pos, actionKey } = this.state; + if (!visible) { + return null; + } + let avoidLaterHidden = true; + setTimeout(() => { + avoidLaterHidden = false; + }, 10); + + const id = uniqueId('ball'); + + return ( + <Balloon + className="lc-ballon" + align="l" + id={this.props.safeId} + safeNode={id} + visible={visible} + style={{ width }} + onVisibleChange={(visible) => { + if (avoidLaterHidden) { + return; + } + if (!visible) { + this.setState({ visible: false }); + } + }} + trigger={<div className="lc-popup-placeholder" style={pos} />} + triggerType="click" + animation={false} + // needAdjust + shouldUpdatePosition + > + <div className="lc-ballon-title">{title}</div> + <div className="lc-ballon-content"> + <PopupService actionKey={actionKey} safeId={id}> + {content} + </PopupService> + </div> + </Balloon> + ); + } +} diff --git a/packages/editor-skeleton/src/components/popup/style.less b/packages/editor-skeleton/src/components/popup/style.less new file mode 100644 index 000000000..b115fd371 --- /dev/null +++ b/packages/editor-skeleton/src/components/popup/style.less @@ -0,0 +1,22 @@ +.lc-popup-placeholder { + position: fixed; + width: 100%; + pointer-events: none; +} + +.lc-ballon { + padding: 10px; + max-width: 640px; + width: 640px; + .lc-ballon-title { + font-size: 14px; + } + .lc-ballon-content { + margin-top: 10px; + // width: 300px; + } + .next-balloon-close { + top: 4px; + right: 4px; + } +} diff --git a/packages/editor-skeleton/src/components/settings/index.ts b/packages/editor-skeleton/src/components/settings/index.ts new file mode 100644 index 000000000..be0ed6e8d --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/index.ts @@ -0,0 +1,7 @@ +export * from './settings-pane'; +import './transducers/register'; +import './register'; +import './style.less'; +import SettingsMainView from './settings-primary-view'; + +export default SettingsMainView; diff --git a/packages/editor-skeleton/src/components/settings/main.ts b/packages/editor-skeleton/src/components/settings/main.ts new file mode 100644 index 000000000..8296ad7ef --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/main.ts @@ -0,0 +1,84 @@ +import { EventEmitter } from 'events'; +import { Node, Designer, Selection, SettingTopEntry } from '@ali/lowcode-designer'; +import { Editor, obx, computed } from '@ali/lowcode-editor-core'; + +function generateSessionId(nodes: Node[]) { + return nodes + .map((node) => node.id) + .sort() + .join(','); +} + +export class SettingsMain { + private emitter = new EventEmitter(); + private _sessionId = ''; + @obx.ref private _settings?: SettingTopEntry; + + @computed get length(): number | undefined { + return this._settings?.nodes.length; + } + + @computed get componentMeta() { + return this._settings?.componentMeta; + } + + get settings() { + return this._settings; + } + + private disposeListener: () => void; + + private designer?: Designer; + + constructor(readonly editor: Editor) { + this.init(); + } + + private async init() { + const setupSelection = (selection?: Selection) => { + if (selection) { + this.setup(selection.getNodes()); + } else { + this.setup([]); + } + }; + this.editor.on('designer.selection.change', setupSelection); + this.disposeListener = () => { + this.editor.removeListener('designer.selection.change', setupSelection); + }; + const designer = await this.editor.onceGot(Designer); + this.designer = designer; + setupSelection(designer.currentSelection); + } + + private setup(nodes: Node[]) { + // check nodes change + const sessionId = generateSessionId(nodes); + if (sessionId === this._sessionId) { + return; + } + this._sessionId = sessionId; + if (nodes.length < 1) { + this._settings = undefined; + return; + } + + if (!this.designer) { + this.designer = nodes[0].document.designer; + } + + this._settings = this.designer.createSettingEntry(this.editor, nodes); + } + + onceOutlineVisible(fn: () => void): () => void { + this.emitter.on('outline-visible', fn); + return () => { + this.emitter.removeListener('outline-visible', fn); + }; + } + + purge() { + this.disposeListener(); + this.emitter.removeAllListeners(); + } +} diff --git a/packages/editor-skeleton/src/components/settings/package.json b/packages/editor-skeleton/src/components/settings/package.json new file mode 100644 index 000000000..2a0fab24e --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/package.json @@ -0,0 +1,49 @@ +{ + "name": "@ali/lowcode-plugin-settings-pane", + "version": "0.8.10", + "description": "Settings pane for Ali lowCode engine", + "files": [ + "es", + "lib" + ], + "main": "lib/index.js", + "module": "es/index.js", + "scripts": { + "build": "build-scripts build --skip-demo", + "test": "ava", + "test:snapshot": "ava --update-snapshots" + }, + "dependencies": { + "@ali/lowcode-designer": "^0.9.2", + "@ali/lowcode-editor-core": "^0.8.5", + "@ali/lowcode-globals": "^0.9.2", + "@ali/lowcode-plugin-outline-pane": "^0.8.8", + "@ali/ve-stage-box": "^4.0.0", + "@alifd/next": "^1.19.16", + "classnames": "^2.2.6", + "react": "^16" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.18", + "@types/classnames": "^2.2.7", + "@types/node": "^13.7.1", + "@types/react": "^16", + "build-plugin-component": "^0.2.10", + "build-plugin-fusion": "^0.1.1", + "build-plugin-moment-locales": "^0.1.0" + }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + }, + "license": "MIT", + "publishConfig": { + "registry": "https://registry.npm.alibaba-inc.com" + } +} diff --git a/packages/editor-skeleton/src/components/settings/register.ts b/packages/editor-skeleton/src/components/settings/register.ts new file mode 100644 index 000000000..1ff61b101 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/register.ts @@ -0,0 +1,30 @@ +import { registerSetter } from '@ali/lowcode-editor-core'; +import ArraySetter from '../array-setter'; +import ObjectSetter from '../object-setter'; +import MixedSetter from '../mixed-setter'; +import { isPlainObject } from '@ali/lowcode-utils'; + +registerSetter('ArraySetter', { + component: ArraySetter, + defaultProps: {}, + title: 'ArraySetter', // TODO + condition: (field: any) => { + const v = field.getValue(); + return v == null || Array.isArray(v); + }, + initialValue: [], + recommend: true, +}); +registerSetter('ObjectSetter', { + component: ObjectSetter, + // todo: defaultProps + defaultProps: {}, + title: 'ObjectSetter', // TODO + condition: (field: any) => { + const v = field.getValue(); + return v == null || isPlainObject(v); + }, + initialValue: {}, + recommend: true, +}); +registerSetter('MixedSetter', MixedSetter); diff --git a/packages/editor-skeleton/src/components/settings/settings-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-pane.tsx new file mode 100644 index 000000000..9b6e377a6 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/settings-pane.tsx @@ -0,0 +1,152 @@ +import { Component } from 'react'; +import { intl, shallowIntl, createSetterContent, observer } from '@ali/lowcode-editor-core'; +import { createContent } from '@ali/lowcode-utils'; +import { Field, createField } from '../field'; +import PopupService from '../popup'; +import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer'; +import { isSetterConfig, CustomView } from '@ali/lowcode-types'; + +@observer +class SettingFieldView extends Component<{ field: SettingField }> { + render() { + const { field } = this.props; + const { extraProps } = field; + const { condition, defaultValue } = extraProps; + const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; + if (!visible) { + return null; + } + const { setter } = field; + + let setterProps: any = {}; + let setterType: any; + if (Array.isArray(setter)) { + setterType = 'MixedSetter'; + setterProps = { + setters: setter, + }; + } else if (isSetterConfig(setter)) { + setterType = setter.componentName; + if (setter.props) { + setterProps = setter.props; + if (typeof setterProps === 'function') { + setterProps = setterProps(field); + } + } + } else if (setter) { + setterType = setter; + } + let value = null; + if (field.type === 'field') { + if (defaultValue != null && !('defaultValue' in setterProps)) { + setterProps.defaultValue = defaultValue; + } + if (field.valueState > 0) { + value = field.getValue(); + } else { + setterProps.multiValue = true; + if (!('placeholder' in setterProps)) { + // FIXME! move to locale file + setterProps.placeholder = intl({ + type: 'i18n', + 'zh-CN': '多种值', + 'en-US': 'Multiple Value', + }); + } + } + } + + // todo: error handling + + return createField( + { + title: field.title, + collapsed: !field.expanded, + onExpandChange: (expandState) => field.setExpanded(expandState), + }, + createSetterContent(setterType, { + ...shallowIntl(setterProps), + forceInline: extraProps.forceInline, + key: field.id, + // === injection + prop: field, // for compatible vision + field, + // === IO + value, // reaction point + onChange: (value: any) => { + this.setState({ + value, + }); + field.setValue(value); + }, + }), + extraProps.forceInline ? 'plain' : extraProps.display, + ); + } +} + +@observer +class SettingGroupView extends Component<{ field: SettingField }> { + shouldComponentUpdate() { + return false; + } + + render() { + const { field } = this.props; + const { extraProps } = field; + const { condition } = extraProps; + const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; + + if (!visible) { + return null; + } + + // todo: split collapsed state | field.items for optimize + return ( + <Field + defaultDisplay="accordion" + title={field.title} + collapsed={!field.expanded} + onExpandChange={(expandState) => { + field.setExpanded(expandState); + }} + > + {field.items.map((item, index) => createSettingFieldView(item, field, index))} + </Field> + ); + } +} + +export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) { + if (isSettingField(item)) { + if (item.isGroup) { + return <SettingGroupView field={item} key={item.id} />; + } else { + return <SettingFieldView field={item} key={item.id} />; + } + } else { + return createContent(item, { key: index, field }); + } +} + +@observer +export class SettingsPane extends Component<{ target: SettingTopEntry | SettingField }> { + shouldComponentUpdate() { + return false; + } + + render() { + const { target } = this.props; + const items = target.items; + return ( + <div className="lc-settings-pane"> + {/* todo: add head for single use */} + <PopupService> + <div className="lc-settings-content"> + {items.map((item, index) => createSettingFieldView(item, target, index))} + </div> + </PopupService> + </div> + ); + } +} diff --git a/packages/editor-skeleton/src/components/settings/settings-primary-view.tsx b/packages/editor-skeleton/src/components/settings/settings-primary-view.tsx new file mode 100644 index 000000000..7a4c49815 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/settings-primary-view.tsx @@ -0,0 +1,121 @@ +import React, { Component } from 'react'; +import { Tab, Breadcrumb } from '@alifd/next'; +import { Title, observer, Editor } from '@ali/lowcode-editor-core'; +import { Node, isSettingField, SettingField } from '@ali/lowcode-designer'; +import { SettingsMain } from './main'; +import { SettingsPane } from './settings-pane'; +import { createIcon } from '@ali/lowcode-utils'; + +@observer +export default class SettingsMainView extends Component<{ editor: Editor }> { + private main = new SettingsMain(this.props.editor); + + shouldComponentUpdate() { + return false; + } + + componentWillUnmount() { + this.main.purge(); + } + + renderBreadcrumb() { + const { settings } = this.main; + if (!settings) { + return null; + } + if (settings.isMultiple) { + return ( + <div className="lc-settings-navigator"> + {createIcon(settings.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})} + <Title title={settings.componentMeta!.title} /> + <span>x {settings.nodes.length}</span> + </div> + ); + } + + let node: Node | null = settings.first; + const items = []; + let l = 3; + while (l-- > 0 && node) { + const props = + l === 2 + ? {} + : { + onMouseOver: hoverNode.bind(null, node, true), + onMouseOut: hoverNode.bind(null, node, false), + onClick: selectNode.bind(null, node), + }; + items.unshift(<Breadcrumb.Item {...props} key={node.id}><Title title={node.title} /></Breadcrumb.Item>); + node = node.parent; + } + + return ( + <div className="lc-settings-navigator"> + {createIcon(this.main.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})} + <Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb> + </div> + ); + } + + render() { + const { settings } = this.main; + if (!settings) { + // 未选中节点,提示选中 或者 显示根节点设置 + return ( + <div className="lc-settings-main"> + <div className="lc-settings-notice"> + <p>请在左侧画布选中节点</p> + </div> + </div> + ); + } + + if (!settings.isSameComponent) { + // todo: future support 获取设置项交集编辑 + return ( + <div className="lc-settings-main"> + <div className="lc-settings-notice"> + <p>请选中同一类型节点编辑</p> + </div> + </div> + ); + } + + const { items } = settings; + if (items.length > 5 || items.some(item => !isSettingField(item) || !item.isGroup)) { + return ( + <div className="lc-settings-main"> + {this.renderBreadcrumb()} + <div className="lc-settings-body"> + <SettingsPane target={settings} /> + </div> + </div> + ); + } + + return ( + <div className="lc-settings-main"> + <Tab + navClassName="lc-settings-tabs" + animation={false} + excessMode="dropdown" + contentClassName="lc-settings-tabs-content" + extra={this.renderBreadcrumb()} + > + {(items as SettingField[]).map(field => ( + <Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}> + <SettingsPane target={field} key={field.id} /> + </Tab.Item> + ))} + </Tab> + </div> + ); + } +} + +function hoverNode(node: Node, flag: boolean) { + node.hover(flag); +} +function selectNode(node: Node) { + node.select(); +} diff --git a/packages/editor-skeleton/src/components/settings/style.less b/packages/editor-skeleton/src/components/settings/style.less new file mode 100644 index 000000000..9b2c2b403 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/style.less @@ -0,0 +1,124 @@ +.lc-settings-main { + position: relative; + height: 100%; + overflow: hidden; + + .lc-settings-notice { + text-align: center; + font-size: 12px; + font-family: PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica,Arial,sans-serif; + color: var(--color-text ,rgba(0,0,0,.6)); + padding: 50px 15px 0; + } + + .lc-settings-navigator { + height: 30px; + display: flex; + align-items: center; + padding-left: 5px; + border-bottom: 1px solid var(--color-line-normal); + .lc-settings-navigator-icon { + width: 16px; + height: 16px; + * { + fill: var(--color-icon-normal, rgba(31, 56, 88, 0.4)); + } + } + .lc-settings-node-breadcrumb { + margin-left: 5px; + .next-breadcrumb { + display: inline-flex; + align-items: stretch; + height: 24px; + } + .next-breadcrumb-item { + display: inline-flex; + align-items: center; + cursor: default; + &:not(:last-child):hover { + cursor: pointer; + } + .next-breadcrumb-text { + font-size: 12px; + } + } + } + } + + .lc-settings-body { + position: absolute; + top: 30px; + right: 0; + left: 0; + bottom: 0; + overflow-y: auto; + } + + // ====== reset fusion-tabs ===== + .lc-settings-tabs { + position: relative; + overflow: visible; + > .next-tabs-nav-extra { + position: absolute !important; + top: 40px !important; + left: 0 !important; + height: 30px; + right: 0; + transform: none !important; + + } + .next-tabs-nav-container { + .next-tabs-nav { + display: flex; + .next-tabs-tab.lc-settings-tab-item { + flex: 1; + min-width: 0; + outline: none; + .next-tabs-tab-inner { + text-align: center; + padding: 12px 0; + } + } + } + } + } + + .lc-settings-tabs-content { + position: absolute; + top: 70px; + left:0; + right: 0; + bottom: 0; + .next-tabs-tabpane { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + overflow-y: auto; + outline: none !important; + box-shadow: none !important; + } + } + .lc-outline-pane { + position: absolute; + z-index: 100; + background-color: white; + top: 0; + bottom: 0; + display: none; + } +} + +.lc-settings-pane { + padding-bottom: 50px; + .next-btn { + line-height: 1 !important; + } +} + +html.lc-cursor-dragging:not(.lowcode-has-fixed-tree) { + .lc-settings-main .lc-outline-pane { + display: block; + } +} diff --git a/packages/editor-skeleton/src/components/settings/transducers/addon-combine.ts b/packages/editor-skeleton/src/components/settings/transducers/addon-combine.ts new file mode 100644 index 000000000..c3072df54 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/transducers/addon-combine.ts @@ -0,0 +1,255 @@ +import { TransformedComponentMetadata, FieldConfig, SettingTarget } from '@ali/lowcode-types'; + +export default function(metadata: TransformedComponentMetadata): TransformedComponentMetadata { + const { componentName, configure = {} } = metadata; + if (componentName === 'Leaf') { + return { + ...metadata, + configure: { + ...configure, + combined: [ + { + name: 'children', + title: { type: 'i18n', 'zh-CN': '内容设置', 'en-US': 'Content' }, + setter: { + componentName: 'MixinSetter', + props: { + // TODO: + setters: [ + { + componentName: 'StringSetter', + props: { + // TODO: textarea mode + multiline: true, + }, + initialValue: '', + }, + { + componentName: 'ExpressionSetter', + initialValue: { + type: 'JSExpression', + value: '', + }, + }, + ], + }, + }, + }, + ], + }, + }; + } + + const { props, events = {}, styles } = configure as any; + const isRoot: boolean = componentName === 'Page' || componentName === 'Component'; + const eventsDefinition: any[] = []; + const supportedLifecycles = + events.supportedLifecycles || + (isRoot + ? [ + { + description: '初始化时', + name: 'constructor', + }, + { + description: '装载后', + name: 'componentDidMount', + }, + { + description: '更新时', + name: 'componentDidMount', + }, + { + description: '卸载时', + name: 'componentWillUnmount', + }, + ] + : null); + if (supportedLifecycles) { + eventsDefinition.push({ + type: 'lifeCycleEvent', + title: '生命周期', + list: supportedLifecycles.map((event: any) => (typeof event === 'string' ? { name: event } : event)), + }); + } + if (events.supportedEvents) { + eventsDefinition.push({ + type: 'events', + title: '事件', + list: (events.supportedEvents || []).map((event: any) => (typeof event === 'string' ? { name: event } : event)), + }); + } + // 通用设置 + const propsGroup = props || []; + propsGroup.push({ + name: '#generals', + title: { type: 'i18n', 'zh-CN': '通用', 'en-US': 'General' }, + items: [ + { + name: 'id', + title: 'ID', + setter: 'StringSetter', + }, + { + name: 'key', + title: 'Key', + // todo: use Mixin + setter: 'StringSetter', + }, + { + name: 'ref', + title: 'Ref', + setter: 'StringSetter', + }, + /* + { + name: '!more', + title: '更多', + setter: 'PropertiesSetter', + },*/ + ], + }); + const combined: FieldConfig[] = [ + { + title: { type: 'i18n', 'zh-CN': '属性', 'en-US': 'Props' }, + name: '#props', + items: propsGroup, + }, + ]; + const stylesGroup: FieldConfig[] = []; + if (styles?.supportClassName) { + stylesGroup.push({ + name: 'className', + title: { type: 'i18n', 'zh-CN': '类名绑定', 'en-US': 'ClassName' }, + setter: 'ClassNameSetter', + }); + } + if (styles?.supportInlineStyle) { + stylesGroup.push({ + name: 'style', + title: { type: 'i18n', 'zh-CN': '行内样式', 'en-US': 'Style' }, + setter: 'StyleSetter', + }); + } + if (stylesGroup.length > 0) { + combined.push({ + name: '#styles', + title: { type: 'i18n', 'zh-CN': '样式', 'en-US': 'Styles' }, + items: stylesGroup, + }); + } + + if (eventsDefinition.length > 0) { + combined.push({ + name: '#events', + title: { type: 'i18n', 'zh-CN': '事件', 'en-US': 'Events' }, + items: [ + { + name: '!events', + title: { type: 'i18n', 'zh-CN': '事件设置', 'en-US': 'Events' }, + setter: { + componentName: 'EventsSetter', + props: { + definition: eventsDefinition, + }, + }, + getValue(field: SettingTarget, val?: any[]) { + // todo: + return val; + }, + + setValue(field: SettingTarget, eventDataList: any[]) { + // todo: + return; + }, + }, + ], + }); + } + + if (isRoot) { + /* + combined.push({ + name: '#advanced', + title: { type: 'i18n', 'zh-CN': '高级', 'en-US': 'Advance' }, + items: [], + }); + */ + } else { + combined.push({ + name: '#advanced', + title: { type: 'i18n', 'zh-CN': '高级', 'en-US': 'Advance' }, + items: [ + { + name: '__condition', + title: { type: 'i18n', 'zh-CN': '条件显示', 'en-US': 'Condition' }, + setter: 'ExpressionSetter', + }, + { + name: '#loop', + title: { type: 'i18n', 'zh-CN': '循环', 'en-US': 'Loop' }, + items: [ + { + name: '__loop', + title: { type: 'i18n', 'zh-CN': '循环数据', 'en-US': 'Loop Data' }, + setter: { + componentName: 'MixinSetter', + props: { + // TODO: + setters: [ + { + componentName: 'JSONSetter', + props: { + mode: 'popup', + placeholder: { type: 'i18n', 'zh-CN': '编辑数据', 'en-US': 'Edit Data' }, + }, + }, + { + componentName: 'ExpressionSetter', + props: { + placeholder: { type: 'i18n', 'zh-CN': '绑定数据', 'en-US': 'Bind Data' }, + }, + }, + ], + }, + }, + }, + { + name: '__loopArgs.0', + title: { type: 'i18n', 'zh-CN': '迭代变量名', 'en-US': 'Loop Item' }, + setter: { + componentName: 'StringSetter', + props: { + placeholder: { type: 'i18n', 'zh-CN': '默认为: item', 'en-US': 'Defaults: item' }, + } + }, + }, + { + name: '__loopArgs.1', + title: { type: 'i18n', 'zh-CN': '索引变量名', 'en-US': 'Loop Index' }, + setter: { + componentName: 'StringSetter', + props: { + placeholder: { type: 'i18n', 'zh-CN': '默认为: index', 'en-US': 'Defaults: index' }, + } + }, + }, + { + name: 'key', + title: 'Key', + setter: 'ExpressionSetter', + }, + ], + }, + ], + }); + } + + return { + ...metadata, + configure: { + ...configure, + combined, + }, + }; +} diff --git a/packages/editor-skeleton/src/components/settings/transducers/parse-props.ts b/packages/editor-skeleton/src/components/settings/transducers/parse-props.ts new file mode 100644 index 000000000..bbf107a81 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/transducers/parse-props.ts @@ -0,0 +1,232 @@ +import { + FieldConfig, + PropConfig, + PropType, + SetterType, + OneOf, + Shape, + ObjectOf, + ArrayOf, + TransformedComponentMetadata, +} from '@ali/lowcode-types'; + +function propConfigToFieldConfig(propConfig: PropConfig): FieldConfig { + const { name, description } = propConfig; + const title = { + label: { + type: 'i18n', + 'en-US': name, + 'zh-CN': description?.slice(0, 10) || name, + }, + tip: description ? `${name} | ${description}` : undefined, + }; + return { + title, + ...propConfig, + setter: propTypeToSetter(propConfig.propType), + }; +} + +function propTypeToSetter(propType: PropType): SetterType { + let typeName: string; + let isRequired: boolean | undefined = false; + if (typeof propType === 'string') { + typeName = propType; + } else { + typeName = propType.type; + isRequired = propType.isRequired; + } + // TODO: use mixinSetter wrapper + switch (typeName) { + case 'string': + return { + componentName: 'StringSetter', + isRequired, + initialValue: '', + }; + + case 'number': + return { + componentName: 'NumberSetter', + isRequired, + initialValue: 0, + }; + case 'bool': + return { + componentName: 'NumberSetter', + isRequired, + initialValue: false, + }; + case 'oneOf': + const dataSource = ((propType as OneOf).value || []).map((value, index) => { + const t = typeof value; + return { + label: t === 'string' || t === 'number' || t === 'boolean' ? String(value) : `value ${index}`, + value, + }; + }); + const componentName = dataSource.length > 4 ? 'SelectSetter' : 'RadioGroupSetter'; + return { + componentName, + props: { dataSource }, + isRequired, + initialValue: dataSource[0] ? dataSource[0].value : null, + }; + + case 'element': + case 'node': // TODO: use Mixin + return { + // slotSetter + componentName: 'NodeSetter', + props: { + mode: typeName, + }, + isRequired, + initialValue: { + type: 'JSSlot', + value: '', + }, + }; + case 'shape': + case 'exact': + const items = (propType as Shape).value.map((item) => propConfigToFieldConfig(item)); + return { + componentName: 'ObjectSetter', + props: { + config: { + items, + extraSetter: typeName === 'shape' ? propTypeToSetter('any') : null, + }, + }, + isRequired, + initialValue: (field: any) => { + const data: any = {}; + items.forEach((item) => { + let initial = item.defaultValue; + if (initial == null && item.setter && typeof item.setter === 'object') { + initial = (item.setter as any).initialValue; + } + data[item.name] = initial ? (typeof initial === 'function' ? initial(field) : initial) : null; + }); + return data; + }, + }; + case 'object': + case 'objectOf': + return { + componentName: 'ObjectSetter', + props: { + config: { + extraSetter: propTypeToSetter(typeName === 'objectOf' ? (propType as ObjectOf).value : 'any'), + }, + }, + isRequired, + }; + case 'array': + case 'arrayOf': + return { + componentName: 'ArraySetter', + props: { + itemSetter: propTypeToSetter(typeName === 'arrayOf' ? (propType as ArrayOf).value : 'any'), + }, + isRequired, + initialValue: [], + }; + case 'func': + return { + componentName: 'FunctionSetter', + isRequired, + initialValue: { + type: 'JSFunction', + value: 'function(){}', + }, + }; + case 'oneOfType': + return { + componentName: 'MixinSetter', + props: { + // TODO: + // setters: (propType as OneOfType).value.map(item => propTypeToSetter(item)), + }, + isRequired, + }; + } + + return { + componentName: 'MixinSetter', + isRequired, + }; +} + +const EVENT_RE = /^on[A-Z][\w]*$/; + +export default function(metadata: TransformedComponentMetadata): TransformedComponentMetadata { + const { configure } = metadata; + if (configure.props) { + return metadata; + } + + if (!metadata.props) { + return { + ...metadata, + configure: { + ...configure, + props: [], + }, + }; + } + const { component = {}, events = {}, styles = {} } = configure; + const supportedEvents: any[] | null = (events as any).supportedEvents ? null : []; + const props: FieldConfig[] = []; + + metadata.props.forEach((prop) => { + const { name, propType, description } = prop; + if ( + name === 'children' && + (component.isContainer || propType === 'node' || propType === 'element' || propType === 'any') + ) { + if (component.isContainer !== false) { + component.isContainer = true; + return; + } + } + + if (EVENT_RE.test(name) && (propType === 'func' || propType === 'any')) { + if (supportedEvents) { + supportedEvents.push({ + name, + description, + }); + (events as any).supportedEvents = supportedEvents; + } + return; + } + + if (name === 'className' && (propType === 'string' || propType === 'any')) { + if ((styles as any).supportClassName == null) { + (styles as any).supportClassName = true; + } + return; + } + + if (name === 'style' && (propType === 'object' || propType === 'any')) { + if ((styles as any).supportInlineStyle == null) { + (styles as any).supportInlineStyle = true; + } + return; + } + + props.push(propConfigToFieldConfig(prop)); + }); + + return { + ...metadata, + configure: { + ...configure, + props, + events, + styles, + component, + }, + }; +} diff --git a/packages/editor-skeleton/src/components/settings/transducers/register.ts b/packages/editor-skeleton/src/components/settings/transducers/register.ts new file mode 100644 index 000000000..0ac451c52 --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/transducers/register.ts @@ -0,0 +1,9 @@ +import { registerMetadataTransducer } from '@ali/lowcode-designer'; +import parseProps from './parse-props'; +import addonCombine from './addon-combine'; + +// parseProps +registerMetadataTransducer(parseProps, 10, 'parse-props'); + +// addon/platform custom +registerMetadataTransducer(addonCombine, 11, 'combine-props'); diff --git a/packages/editor-skeleton/src/components/settings/utils.js b/packages/editor-skeleton/src/components/settings/utils.js new file mode 100644 index 000000000..b8f5bbdcc --- /dev/null +++ b/packages/editor-skeleton/src/components/settings/utils.js @@ -0,0 +1,41 @@ +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 +} + +function combineTransducer(transducer, arr, context) { + if (!transducer && Array.isArray(arr)) { + const [toHot, toNative] = arr; + transducer = { toHot, toNative }; + } + + return { + toHot: (transducer && transducer.toHot || (x => x)).bind(context), // eslint-disable-line + toNative: (transducer && transducer.toNative || (x => x)).bind(context), // eslint-disable-line + }; +} + +export class Transducer { + constructor(context, config) { + this.setterTransducer = combineTransducer( + getTransducerFromSetter(config.setter), + getHotterFromSetter(config.setter), + context, + ); + this.context = context; + } + + toHot(data) { + return this.setterTransducer.toHot(data); + } + + toNative(data) { + return this.setterTransducer.toNative(data); + } +} diff --git a/packages/editor-skeleton/src/components/widget-views.tsx b/packages/editor-skeleton/src/components/widget-views.tsx new file mode 100644 index 000000000..e097c3407 --- /dev/null +++ b/packages/editor-skeleton/src/components/widget-views.tsx @@ -0,0 +1,236 @@ +import { Component, ReactElement } from 'react'; +import classNames from 'classnames'; +import { Title, observer } from '@ali/lowcode-editor-core'; +import { DockProps } from '../types'; +import PanelDock from '../widget/panel-dock'; +import { composeTitle } from '../widget/utils'; +import WidgetContainer from '../widget/widget-container'; +import Panel from '../widget/panel'; +import { IWidget } from '../widget/widget'; +import { SkeletonEvents } from '../skeleton'; + +export function DockView({ title, icon, description, size, className, onClick }: DockProps) { + return ( + <Title + title={composeTitle(title, icon, description)} + className={classNames('lc-dock', className, { + [`lc-dock-${size}`]: size, + })} + onClick={onClick} + /> + ); +} + +@observer +export class PanelDockView extends Component<DockProps & { dock: PanelDock }> { + componentDidMount() { + this.checkActived(); + } + componentDidUpdate() { + this.checkActived(); + } + private lastActived: boolean = false; + checkActived() { + const { dock } = this.props; + if (dock.actived !== this.lastActived) { + this.lastActived = dock.actived; + if (this.lastActived) { + dock.skeleton.postEvent(SkeletonEvents.PANEL_DOCK_ACTIVE, dock.name, dock); + } else { + dock.skeleton.postEvent(SkeletonEvents.PANEL_DOCK_UNACTIVE, dock.name, dock); + } + } + } + + render() { + const { dock, className, onClick, ...props } = this.props; + return DockView({ + ...props, + className: classNames(className, { + actived: dock.actived, + }), + onClick: () => { + onClick && onClick(); + dock.togglePanel(); + }, + }); + } +} + +export class DialogDockView extends Component { + +} + +@observer +export class TitledPanelView extends Component<{ panel: Panel }> { + shouldComponentUpdate() { + return false; + } + componentDidMount() { + this.checkVisible(); + } + componentDidUpdate() { + this.checkVisible(); + } + private lastVisible: boolean = false; + checkVisible() { + const { panel } = this.props; + const currentVisible = panel.inited && panel.visible; + if (currentVisible !== this.lastVisible) { + this.lastVisible = currentVisible; + if (this.lastVisible) { + panel.skeleton.postEvent(SkeletonEvents.PANEL_SHOW, panel.name, panel); + } else { + panel.skeleton.postEvent(SkeletonEvents.PANEL_HIDE, panel.name, panel); + } + } + } + render() { + const { panel } = this.props; + if (!panel.inited) { + return null; + } + return ( + <div className={classNames('lc-titled-panel', { + hidden: !panel.visible, + })}> + <PanelTitle panel={panel} /> + <div className="lc-pane-body">{panel.body}</div> + </div> + ); + } +} + +@observer +export class PanelView extends Component<{ panel: Panel }> { + shouldComponentUpdate() { + return false; + } + componentDidMount() { + this.checkVisible(); + } + componentDidUpdate() { + this.checkVisible(); + } + private lastVisible: boolean = false; + checkVisible() { + const { panel } = this.props; + const currentVisible = panel.inited && panel.visible; + if (currentVisible !== this.lastVisible) { + this.lastVisible = currentVisible; + if (this.lastVisible) { + panel.skeleton.postEvent(SkeletonEvents.PANEL_SHOW, panel.name, panel); + // FIXME! remove this line + panel.skeleton.postEvent('leftPanel.show' as any, panel.name, panel); + } else { + panel.skeleton.postEvent(SkeletonEvents.PANEL_HIDE, panel.name, panel); + // FIXME! remove this line + panel.skeleton.postEvent('leftPanel.hide' as any, panel.name, panel); + } + } + } + render() { + const { panel } = this.props; + if (!panel.inited) { + return null; + } + return ( + <div + className={classNames('lc-panel', { + hidden: !panel.visible, + })} + > + {panel.body} + </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(<PanelView 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 WidgetView extends Component<{ widget: IWidget }> { + shouldComponentUpdate() { + return false; + } + componentDidMount() { + this.checkVisible(); + } + componentDidUpdate() { + this.checkVisible(); + } + private lastVisible: boolean = false; + checkVisible() { + const { widget } = this.props; + const currentVisible = widget.visible; + if (currentVisible !== this.lastVisible) { + this.lastVisible = currentVisible; + if (this.lastVisible) { + widget.skeleton.postEvent(SkeletonEvents.WIDGET_SHOW, widget.name, widget); + } else { + widget.skeleton.postEvent(SkeletonEvents.WIDGET_SHOW, widget.name, widget); + } + } + } + render() { + const { widget } = this.props; + if (!widget.visible) { + return null; + } + return widget.body; + } +} diff --git a/packages/editor-skeleton/src/config/skeleton.ts b/packages/editor-skeleton/src/config/skeleton.ts deleted file mode 100644 index ff8b4c563..000000000 --- a/packages/editor-skeleton/src/config/skeleton.ts +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/packages/editor-skeleton/src/config/utils.ts b/packages/editor-skeleton/src/config/utils.ts deleted file mode 100644 index ff8b4c563..000000000 --- a/packages/editor-skeleton/src/config/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/packages/editor-skeleton/src/dock.ts b/packages/editor-skeleton/src/dock.ts new file mode 100644 index 000000000..affa84d98 --- /dev/null +++ b/packages/editor-skeleton/src/dock.ts @@ -0,0 +1,87 @@ +import { ReactNode, createElement } from 'react'; +import { obx } from '@ali/lowcode-editor-core'; +import { uniqueId, createContent } from '@ali/lowcode-utils'; +import { DockConfig } from "./types"; +import { Skeleton } from './skeleton'; +import { DockView, WidgetView } from './components/widget-views'; +import { IWidget } from './widget/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; + } + + get content(): ReactNode { + return createElement(WidgetView, { + widget: this, + key: this.id, + }); + } + + private inited: boolean = false; + private _body: ReactNode; + get body() { + if (this.inited) { + return this._body; + } + + const { props, content, contentProps } = this.config; + + if (content) { + this._body = createContent(content, { + ...contentProps, + config: this.config, + editor: this.skeleton.editor, + }); + } else { + this._body = createElement(DockView, props); + } + return this._body; + } + + constructor(readonly skeleton: Skeleton, readonly 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); + } + + toggle() { + this.setVisible(!this._visible); + } +} diff --git a/packages/editor-skeleton/src/global.scss b/packages/editor-skeleton/src/global.scss deleted file mode 100644 index 6ea0487ea..000000000 --- a/packages/editor-skeleton/src/global.scss +++ /dev/null @@ -1,33 +0,0 @@ -body { - font-family: PingFangSC-Regular, Roboto, Helvetica Neue, Helvetica, Tahoma, - Arial, PingFang SC-Light, Microsoft YaHei; - font-size: 12px; - padding: 0; - margin: 0; - * { - box-sizing: border-box; - } - color: $color-text1-3; -} - -.next-loading { - .next-loading-wrap { - height: 100%; - } -} -.lowcode-editor { - .lowcode-main-content { - position: absolute; - top: 50px; - left: 0; - right: 0; - bottom: 0; - display: flex; - background-color: rgba(31, 56, 88, 0.06); - } - .lowcode-center-area { - flex: 1; - display: flex; - flex-direction: column; - } -} diff --git a/packages/editor-skeleton/src/icons/convert.tsx b/packages/editor-skeleton/src/icons/convert.tsx new file mode 100644 index 000000000..2db5e2063 --- /dev/null +++ b/packages/editor-skeleton/src/icons/convert.tsx @@ -0,0 +1,16 @@ +import { SVGIcon, IconProps } from "@ali/lowcode-utils"; + +export function IconConvert(props: IconProps) { + return ( + <SVGIcon viewBox="0 0 1024 1024" {...props}> + <path d="M508.16 889.6C291.84 889.6 115.2 714.24 115.2 497.92 115.2 281.6 291.84 106.24 509.44 106.24c43.52 0 85.76 6.4 124.16 20.48l-10.24 30.72c-35.84-11.52-72.96-17.92-113.92-17.92-199.68 0-362.24 161.28-362.24 359.68s162.56 358.4 360.96 358.4 359.68-161.28 359.68-359.68c0-66.56-17.92-131.84-51.2-185.6L844.8 294.4c37.12 60.16 56.32 130.56 56.32 203.52-1.28 216.32-176.64 391.68-392.96 391.68z" /> + <path d="M627.2 140.8m-15.36 0a15.36 15.36 0 1 0 30.72 0 15.36 15.36 0 1 0-30.72 0Z" /> + <path d="M832 304.64m-15.36 0a15.36 15.36 0 1 0 30.72 0 15.36 15.36 0 1 0-30.72 0Z" /> + <path d="M348.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" /> + <path d="M508.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" /> + <path d="M668.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" /> + </SVGIcon> + ); +} + +IconConvert.displayName = 'Convert'; diff --git a/packages/editor-skeleton/src/index.ts b/packages/editor-skeleton/src/index.ts index 061621d42..923bd59cb 100644 --- a/packages/editor-skeleton/src/index.ts +++ b/packages/editor-skeleton/src/index.ts @@ -1,9 +1 @@ -import Skeleton from './skeleton'; -import Panel from './components/Panel'; -import TopIcon from './components/TopIcon'; -import TopPlugin from './components/TopPlugin'; -import LeftPlugin from './components/LeftPlugin'; - -export default Skeleton; - -export { Panel, TopIcon, TopPlugin, LeftPlugin }; +export { Workbench } from './layouts/workbench'; diff --git a/packages/editor-skeleton/src/layouts/CenterArea/index.scss b/packages/editor-skeleton/src/layouts/CenterArea/index.scss deleted file mode 100644 index 40a1806a6..000000000 --- a/packages/editor-skeleton/src/layouts/CenterArea/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.lowcode-center-area { - padding: 0; -} diff --git a/packages/editor-skeleton/src/layouts/CenterArea/index.tsx b/packages/editor-skeleton/src/layouts/CenterArea/index.tsx deleted file mode 100644 index 3261b4bbe..000000000 --- a/packages/editor-skeleton/src/layouts/CenterArea/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { PureComponent } from 'react'; -import Editor, { AreaManager } from '@ali/lowcode-editor-core'; -import './index.scss'; - -export interface CenterAreaProps { - editor: Editor; -} - -export default class CenterArea extends PureComponent<CenterAreaProps> { - static displayName = 'LowcodeCenterArea'; - - private editor: Editor; - - private areaManager: AreaManager; - - constructor(props) { - super(props); - this.editor = props.editor; - this.areaManager = new AreaManager(this.editor, 'centerArea'); - } - - componentDidMount(): void { - this.editor.on('skeleton.update', this.handleSkeletonUpdate); - } - - componentWillUnmount(): void { - this.editor.off('skeleton.update', this.handleSkeletonUpdate); - } - - handleSkeletonUpdate = (): void => { - // 当前区域插件状态改变是更新区域 - if (this.areaManager.isPluginStatusUpdate()) { - this.forceUpdate(); - } - }; - - render(): React.ReactNode { - const visiblePluginList = this.areaManager.getVisiblePluginList(); - return ( - <div className="lowcode-center-area"> - {visiblePluginList.map( - (item): React.ReactNode => { - const Comp = this.areaManager.getPluginClass(item.pluginKey); - if (Comp) { - return ( - <Comp - key={item.pluginKey} - editor={this.editor} - config={item} - {...item.pluginProps} - /> - ); - } - return null; - }, - )} - </div> - ); - } -} diff --git a/packages/editor-skeleton/src/layouts/LeftArea/index.scss b/packages/editor-skeleton/src/layouts/LeftArea/index.scss deleted file mode 100644 index 8c69f42fc..000000000 --- a/packages/editor-skeleton/src/layouts/LeftArea/index.scss +++ /dev/null @@ -1,23 +0,0 @@ -.lowcode-left-area-nav { - width: 50px; - height: 100%; - background-color: $card-background; - border-right: 2px solid $color-line1-1; - position: relative; - .top-area { - position: absolute; - top: 0; - width: 100%; - padding: 12px 0; - background-color: $card-background; - max-height: 100%; - } - .bottom-area { - position: absolute; - bottom: 0; - width: 100%; - padding: 12px 0; - background-color: $card-background; - max-height: calc(100% - 20px); - } -} diff --git a/packages/editor-skeleton/src/layouts/LeftArea/index.tsx b/packages/editor-skeleton/src/layouts/LeftArea/index.tsx deleted file mode 100644 index 805a5e014..000000000 --- a/packages/editor-skeleton/src/layouts/LeftArea/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Nav from './nav'; -import Panel from './panel'; - -export default { - Nav, - Panel, -}; diff --git a/packages/editor-skeleton/src/layouts/LeftArea/nav.tsx b/packages/editor-skeleton/src/layouts/LeftArea/nav.tsx deleted file mode 100644 index 735b77a27..000000000 --- a/packages/editor-skeleton/src/layouts/LeftArea/nav.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { PureComponent } from 'react'; -import LeftPlugin from '../../components/LeftPlugin'; -import Editor, { utils, AreaManager } from '@ali/lowcode-editor-core'; -import { PluginConfig } from '@ali/lowcode-editor-core/lib/definitions'; - -import './index.scss'; - -const { isEmpty } = utils; - -export interface LeftAreaNavProps { - editor: Editor; -} - -export interface LeftAreaNavState { - activeKey: string; -} - -export default class LeftAreaNav extends PureComponent< - LeftAreaNavProps, - LeftAreaNavState -> { - static displayName = 'LowcodeLeftAreaNav'; - - private editor: Editor; - - private areaManager: AreaManager; - - constructor(props) { - super(props); - this.editor = props.editor; - this.areaManager = new AreaManager(this.editor, 'leftArea'); - - this.state = { - activeKey: 'none', - }; - } - - componentDidMount(): void { - this.editor.on('skeleton.update', this.handleSkeletonUpdate); - this.editor.on('leftNav.change', this.handlePluginChange); - const visiblePanelPluginList = this.areaManager.getVisiblePluginList( - 'IconPanel', - ); - const defaultKey = - (visiblePanelPluginList[0] && visiblePanelPluginList[0].pluginKey) || - 'componentAttr'; - this.handlePluginChange(defaultKey); - } - - componentWillUnmount(): void { - this.editor.off('skeleton.update', this.handleSkeletonUpdate); - this.editor.off('leftNav.change', this.handlePluginChange); - } - - handleSkeletonUpdate = (): void => { - // 当前区域插件状态改变是更新区域 - if (this.areaManager.isPluginStatusUpdate()) { - this.forceUpdate(); - } - }; - - handlePluginChange = (key: string): void => { - const { activeKey } = this.state; - const plugins = this.editor.plugins; - const prePlugin = plugins[activeKey]; - const nextPlugin = plugins[key]; - if (activeKey === 'none') { - if (nextPlugin) { - nextPlugin.open().then((): void => { - this.updateActiveKey(key); - }); - } - } else if (activeKey === key) { - if (prePlugin) { - prePlugin.close().then((): void => { - this.updateActiveKey('none'); - }); - } - } else if (prePlugin) { - // 先关后开 - prePlugin.close().then((): void => { - if (nextPlugin) { - nextPlugin.open().then((): void => { - this.updateActiveKey(key); - }); - } - }); - } - }; - - handlePluginClick = (item: PluginConfig): void => { - if (item.type === 'PanelIcon') { - this.handlePluginChange(item.pluginKey); - } - }; - - updateActiveKey = (key: string): void => { - this.editor.set('leftNav', key); - this.setState({ activeKey: key }); - this.editor.emit('leftPanel.show', key); - }; - - renderPluginList = (list: PluginConfig[] = []): React.ReactElement[] => { - const { activeKey } = this.state; - return list.map( - (item): React.ReactElement => { - const pluginStatus = this.areaManager.getPluginStatus(item.pluginKey); - const pluginClass = this.areaManager.getPluginClass(item.pluginKey); - return ( - <LeftPlugin - key={item.pluginKey} - config={item} - editor={this.editor} - pluginClass={pluginClass} - onClick={(): void => this.handlePluginClick(item)} - active={activeKey === item.pluginKey} - {...pluginStatus} - /> - ); - }, - ); - }; - - render(): React.ReactNode { - const topList: PluginConfig[] = []; - const bottomList: PluginConfig[] = []; - const visiblePluginList = this.areaManager.getVisiblePluginList(); - if (isEmpty(visiblePluginList)) { - return null; - } - visiblePluginList.forEach((item): void => { - const align = - item.props && item.props.align === 'bottom' ? 'bottom' : 'top'; - if (align === 'bottom') { - bottomList.push(item); - } else { - topList.push(item); - } - }); - - return ( - <div className="lowcode-left-area-nav"> - <div className="bottom-area">{this.renderPluginList(bottomList)}</div> - <div className="top-area">{this.renderPluginList(topList)}</div> - </div> - ); - } -} diff --git a/packages/editor-skeleton/src/layouts/LeftArea/panel.tsx b/packages/editor-skeleton/src/layouts/LeftArea/panel.tsx deleted file mode 100644 index ccb81cfe0..000000000 --- a/packages/editor-skeleton/src/layouts/LeftArea/panel.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { PureComponent, Fragment } from 'react'; -import Editor, { AreaManager } from '@ali/lowcode-editor-core'; -import Panel from '../../components/Panel'; - -import './index.scss'; - -export interface LeftAreaPanelProps { - editor: Editor; -} - -export interface LeftAreaPanelState { - activeKey: string; -} - -export default class LeftAreaPanel extends PureComponent< - LeftAreaPanelProps, - LeftAreaPanelState -> { - static displayName = 'LowcodeLeftAreaPanel'; - - private editor: Editor; - - private areaManager: AreaManager; - - constructor(props) { - super(props); - this.editor = props.editor; - this.areaManager = new AreaManager(this.editor, 'leftArea'); - - this.state = { - activeKey: 'none', - }; - } - - componentDidMount(): void { - this.editor.on('skeleton.update', this.handleSkeletonUpdate); - this.editor.on('leftPanel.show', this.handlePluginChange); - } - - componentWillUnmount(): void { - this.editor.off('skeleton.update', this.handleSkeletonUpdate); - this.editor.off('leftPanel.show', this.handlePluginChange); - } - - handleSkeletonUpdate = (): void => { - // 当前区域插件状态改变是更新区域 - if (this.areaManager.isPluginStatusUpdate('PanelIcon')) { - this.forceUpdate(); - } - }; - - handlePluginChange = (key: string): void => { - this.setState({ - activeKey: key, - }); - }; - - render(): React.ReactNode { - const { activeKey } = this.state; - const list = this.areaManager.getVisiblePluginList('PanelIcon'); - - return ( - <Fragment> - {list.map( - (item): React.ReactNode => { - const Comp = this.areaManager.getPluginClass(item.pluginKey); - if (Comp) { - return ( - <Panel - key={item.pluginKey} - visible={item.pluginKey === activeKey} - {...(item.props && item.props.panelProps)} - > - <Comp - editor={this.editor} - config={item} - {...item.pluginProps} - /> - </Panel> - ); - } - return null; - }, - )} - </Fragment> - ); - } -} diff --git a/packages/editor-skeleton/src/layouts/RightArea/index.scss b/packages/editor-skeleton/src/layouts/RightArea/index.scss deleted file mode 100644 index 7373933b0..000000000 --- a/packages/editor-skeleton/src/layouts/RightArea/index.scss +++ /dev/null @@ -1,39 +0,0 @@ -.lowcode-right-area { - width: 262px; - height: 100%; - background-color: $card-background; - border-left: 2px solid $color-line1-1; - - .right-panel { - overflow: auto; - // border-top: 2px solid $color-line1-1; - } - - //tab定义 - .right-tabs.next-tabs { - .next-tabs-nav { - width: 100%; - .next-tabs-tab-inner { - padding-left: 0; - padding-right: 0; - } - .right-plugin-title { - text-align: center; - &.locked { - color: red !important; - } - &.active { - color: $color-brand1-9 !important; - } - &.disabled { - cursor: not-allowed; - color: $color-text1-1; - } - .next-icon { - line-height: 15px; - margin-right: 2px; - } - } - } - } -} diff --git a/packages/editor-skeleton/src/layouts/RightArea/index.tsx b/packages/editor-skeleton/src/layouts/RightArea/index.tsx deleted file mode 100644 index 5925284da..000000000 --- a/packages/editor-skeleton/src/layouts/RightArea/index.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { PureComponent } from 'react'; -import { Tab, Badge, Icon } from '@alifd/next'; -import classNames from 'classnames'; -import Editor, { AreaManager, utils } from '@ali/lowcode-editor-core'; -import { PluginConfig } from '@ali/lowcode-editor-core/lib/definitions'; -import './index.scss'; - -const { isEmpty } = utils; - -export interface RightAreaProps { - editor: Editor; -} - -export interface RightAreaState { - activeKey: string; -} - -export default class RightArea extends PureComponent< - RightAreaProps, - RightAreaState -> { - static displayName = 'LowcodeRightArea'; - - private editor: Editor; - - private areaManager: AreaManager; - - constructor(props) { - super(props); - this.editor = props.editor; - this.areaManager = new AreaManager(this.editor, 'rightArea'); - this.state = { - activeKey: '', - }; - } - - componentDidMount(): void { - this.editor.on('skeleton.update', this.handleSkeletonUpdate); - this.editor.on('rightNav.change', this.handlePluginChange); - const visiblePluginList = this.areaManager.getVisiblePluginList('TabPanel'); - const defaultKey = - (visiblePluginList[0] && visiblePluginList[0].pluginKey) || - 'componentAttr'; - this.handlePluginChange(defaultKey, true); - } - - componentWillUnmount(): void { - this.editor.off('skeleton.update', this.handleSkeletonUpdate); - this.editor.off('rightNav.change', this.handlePluginChange); - } - - handleSkeletonUpdate = (): void => { - // 当前区域插件状态改变是更新区域 - if (this.areaManager.isPluginStatusUpdate()) { - const activeKey = this.state.activeKey; - const activePluginStatus = this.areaManager.getPluginStatus(activeKey); - if (activePluginStatus && activePluginStatus.visible) { - this.forceUpdate(); - } else { - const currentPlugin = this.areaManager.getPlugin(activeKey); - if (currentPlugin) { - currentPlugin.close().then((): void => { - this.setState( - { - activeKey: '', - }, - (): void => { - const visiblePluginList = this.areaManager.getVisiblePluginList( - 'TabPanel', - ); - const firstPlugin = visiblePluginList && visiblePluginList[0]; - if (firstPlugin) { - this.handlePluginChange(firstPlugin.pluginKey); - } - }, - ); - }); - } - } - } - }; - - handlePluginChange = (key: string, isinit?: boolean): void => { - const activeKey = this.state.activeKey; - const currentPlugin = this.areaManager.getPlugin(activeKey); - const nextPlugin = this.areaManager.getPlugin(key); - const openPlugin = (): void => { - if (!nextPlugin) { - console.error(`plugin ${key} has not regist in the editor`); - return; - } - nextPlugin.open().then((): void => { - this.editor.set('rightNav', key); - this.setState({ - activeKey: key, - }); - }); - }; - if (key === activeKey && !isinit) return; - if (currentPlugin) { - currentPlugin.close().then((): void => { - openPlugin(); - }); - } else { - openPlugin(); - } - }; - - renderTabTitle = (config: PluginConfig): React.ReactElement => { - const { icon, title } = config.props || {}; - const pluginStatus = this.editor.pluginStatus[config.pluginKey]; - const { marked, disabled, locked } = pluginStatus; - const active = this.state.activeKey === config.pluginKey; - - const renderTitle = (): React.ReactElement => ( - <div - className={classNames('right-plugin-title', { - active, - locked, - disabled, - })} - > - {!!icon && <Icon size="xs" type={icon} />} - {title} - </div> - ); - if (marked) { - return <Badge dot>{renderTitle()}</Badge>; - } - return renderTitle(); - }; - - renderTabPanels = (list: PluginConfig[], height: string): React.ReactNode => { - if (isEmpty(list)) { - return null; - } - return ( - <Tab - className="right-tabs" - style={{ - height, - }} - activeKey={this.state.activeKey} - lazyLoad={false} - onChange={this.handlePluginChange} - > - {list.map( - (item): React.ReactNode => { - const Comp = this.areaManager.getPluginClass(item.pluginKey); - if (Comp) { - return ( - <Tab.Item - key={item.pluginKey} - title={this.renderTabTitle(item)} - disabled={this.editor.pluginStatus[item.pluginKey].disabled} - style={{ - width: `${100 / list.length}%`, - }} - > - <Comp - editor={this.editor} - config={item} - {...item.pluginProps} - /> - </Tab.Item> - ); - } - return null; - }, - )} - </Tab> - ); - }; - - renderPanels = (list: PluginConfig[], height: string): React.ReactNode => { - return list.map( - (item): React.ReactNode => { - const Comp = this.areaManager.getPluginClass(item.pluginKey); - if (Comp) { - return ( - <div - className="right-panel" - style={{ height }} - key={item.pluginKey} - > - <Comp editor={this.editor} config={item} {...item.pluginProps} /> - </div> - ); - } - return null; - }, - ); - }; - - render(): React.ReactNode { - const tabList = this.areaManager.getVisiblePluginList('TabPanel'); - const panelList = this.areaManager.getVisiblePluginList('Panel'); - if (isEmpty(panelList) && isEmpty(tabList)) { - return null; - } else if (tabList.length === 1) { - panelList.unshift(tabList[0]); - tabList.splice(0, 1); - } - const height = `${Math.floor( - 100 / (panelList.length + (tabList.length > 0 ? 1 : 0)), - )}%`; - return ( - <div className="lowcode-right-area"> - {this.renderTabPanels(tabList, height)} - {this.renderPanels(panelList, height)} - </div> - ); - } -} diff --git a/packages/editor-skeleton/src/layouts/TopArea/index.scss b/packages/editor-skeleton/src/layouts/TopArea/index.scss deleted file mode 100644 index c0a132f84..000000000 --- a/packages/editor-skeleton/src/layouts/TopArea/index.scss +++ /dev/null @@ -1,30 +0,0 @@ -.lowcode-top-area { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 50px; - background-color: $card-background; - border-bottom: 2px solid $color-line1-1; - user-select: none; - .divider { - max-width: 0; - margin: 12px 16px; - height: 24px; - border-right: 1px solid $color-line1-2; - } - .next-col { - text-align: center; - } - .left-area { - padding: 0 16px; - } - .right-area { - position: absolute; - right: 0; - top: 0; - padding: 0 16px; - height: 100%; - background: $card-background; - } -} diff --git a/packages/editor-skeleton/src/layouts/TopArea/index.tsx b/packages/editor-skeleton/src/layouts/TopArea/index.tsx deleted file mode 100644 index 68c73f202..000000000 --- a/packages/editor-skeleton/src/layouts/TopArea/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { PureComponent } from 'react'; -import { Grid } from '@alifd/next'; -import Editor, { AreaManager } from '@ali/lowcode-editor-core'; -import { PluginConfig } from '@ali/lowcode-editor-core/lib/definitions'; -import TopPlugin from '../../components/TopPlugin'; -import './index.scss'; - -const { Row, Col } = Grid; - -export interface TopAreaProps { - editor: Editor; -} - -export default class TopArea extends PureComponent<TopAreaProps> { - static displayName = 'LowcodeTopArea'; - - private areaManager: AreaManager; - - private editor: Editor; - - constructor(props) { - super(props); - this.editor = props.editor; - this.areaManager = new AreaManager(props.editor, 'topArea'); - } - - componentDidMount(): void { - this.editor.on('skeleton.update', this.handleSkeletonUpdate); - } - - componentWillUnmount(): void { - this.editor.off('skeleton.update', this.handleSkeletonUpdate); - } - - handleSkeletonUpdate = (): void => { - // 当前区域插件状态改变是更新区域 - if (this.areaManager.isPluginStatusUpdate()) { - this.forceUpdate(); - } - }; - - renderPluginList = (list: PluginConfig[] = []): React.ReactElement[] => { - return list.map( - (item, idx): React.ReactElement => { - const isDivider = item.type === 'Divider'; - const PluginClass = this.areaManager.getPluginClass(item.pluginKey); - return ( - <Col - className={isDivider ? 'divider' : ''} - key={isDivider ? idx : item.pluginKey} - style={{ - width: (item.props && item.props.width) || 36, - flex: 'none', - }} - > - {!isDivider && ( - <TopPlugin - config={item} - pluginClass={PluginClass} - editor={this.editor} - /> - )} - </Col> - ); - }, - ); - }; - - render(): React.ReactNode { - const leftList: PluginConfig[] = []; - const rightList: PluginConfig[] = []; - const visiblePluginList = this.areaManager.getVisiblePluginList(); - visiblePluginList.forEach((item): void => { - const align = - item.props && item.props.align === 'right' ? 'right' : 'left'; - // 分隔符不允许相邻 - if (item.type === 'Divider') { - const currList = align === 'right' ? rightList : leftList; - if ( - currList.length === 0 || - currList[currList.length - 1].type === 'Divider' - ) - return; - } - if (align === 'right') { - rightList.push(item); - } else { - leftList.push(item); - } - }); - return ( - <div className="lowcode-top-area"> - <div className="left-area"> - <Row>{this.renderPluginList(leftList)}</Row> - </div> - <div className="right-area"> - <Row justify="end">{this.renderPluginList(rightList)}</Row> - </div> - </div> - ); - } -} diff --git a/packages/editor-skeleton/src/layouts/bottom-area.tsx b/packages/editor-skeleton/src/layouts/bottom-area.tsx new file mode 100644 index 000000000..08bc337e2 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/bottom-area.tsx @@ -0,0 +1,37 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import Area from '../area'; +import Panel from '../widget/panel'; + +@observer +export default class BottomArea extends Component<{ area: Area<any, Panel> }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + if (area.isEmpty()) { + return null; + } + return ( + <div className={classNames('lc-bottom-area', { + 'lc-area-visible': area.visible, + })}> + <Contents area={area} /> + </div> + ); + } +} + +@observer +class Contents extends Component<{ area: Area<any, Panel> }> { + render() { + const { area } = this.props; + return ( + <Fragment> + {area.container.items.map((item) => item.content)} + </Fragment> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/left-area.tsx b/packages/editor-skeleton/src/layouts/left-area.tsx new file mode 100644 index 000000000..00a83a328 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/left-area.tsx @@ -0,0 +1,41 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import Area from '../area'; + +@observer +export default class LeftArea extends Component<{ area: Area }> { + render() { + const { area } = this.props; + return ( + <div className={classNames("lc-left-area", { + 'lc-area-visible': area.visible + })}> + <Contents area={area} /> + </div> + ); + } +} + + +@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 ( + <Fragment> + <div className="lc-left-area-top">{top}</div> + <div className="lc-left-area-bottom">{bottom}</div> + </Fragment> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx b/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx new file mode 100644 index 000000000..c5ea01b53 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx @@ -0,0 +1,50 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import { Button, Icon } from '@alifd/next'; +import Area from '../area'; +import { PanelConfig } from '../types'; +import Panel from '../widget/panel'; + +@observer +export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, Panel> }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + 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> + <Contents area={area} /> + </div> + ); + } +} + +@observer +class Contents extends Component<{ area: Area<PanelConfig, Panel> }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( + <Fragment> + {area.container.items.map((panel) => panel.content)} + </Fragment> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/left-float-pane.tsx b/packages/editor-skeleton/src/layouts/left-float-pane.tsx new file mode 100644 index 000000000..84feacd6f --- /dev/null +++ b/packages/editor-skeleton/src/layouts/left-float-pane.tsx @@ -0,0 +1,73 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import { Button, Icon } from '@alifd/next'; +import Area from '../area'; +import Panel from '../widget/panel'; + +@observer +export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> { + shouldComponentUpdate() { + return false; + } + + private dispose?: () => void; + componentDidMount() { + const { area } = this.props; + const triggerClose = () => area.setVisible(false); + area.skeleton.editor.on('designer.dragstart', triggerClose); + this.dispose = () => { + area.skeleton.editor.removeListener('designer.dragstart', triggerClose); + } + } + + componentWillUnmount() { + this.dispose?.(); + } + + render() { + const { area } = this.props; + // TODO: add focusingManager + // focusin set focus (push|replace) + // focusout remove focus + // onEsc + const width = area.current?.config.props?.width; + const style = width ? { + width + } : undefined; + return ( + <div + className={classNames('lc-left-float-pane', { + 'lc-area-visible': area.visible, + })} + style={style} + > + <Button + text + className="lc-pane-close" + onClick={() => { + area.setVisible(false); + }} + > + <Icon type="close" /> + </Button> + <Contents area={area} /> + </div> + ); + } +} + +@observer +class Contents extends Component<{ area: Area<any, Panel> }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( + <Fragment> + {area.container.items.map((panel) => panel.content)} + </Fragment> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/main-area.tsx b/packages/editor-skeleton/src/layouts/main-area.tsx new file mode 100644 index 000000000..891053427 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/main-area.tsx @@ -0,0 +1,21 @@ +import { Component } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import Area from '../area'; +import Panel from '../widget/panel'; +import Widget from '../widget/widget'; + +@observer +export default class MainArea extends Component<{ area: Area<any, Panel | Widget> }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( + <div className={classNames('lc-main-area')}> + {area.container.items.map((item) => item.content)} + </div> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/right-area.tsx b/packages/editor-skeleton/src/layouts/right-area.tsx new file mode 100644 index 000000000..9379f55b1 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/right-area.tsx @@ -0,0 +1,35 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import Area from '../area'; +import Panel from '../widget/panel'; + +@observer +export default class RightArea extends Component<{ area: Area<any, Panel> }> { + shouldComponentUpdate() { + return false; + } + render() { + const { area } = this.props; + return ( + <div className={classNames('lc-right-area', { + 'lc-area-visible': area.visible, + })}> + <Contents area={area} /> + </div> + ); + } +} + + +@observer +class Contents extends Component<{ area: Area<any, Panel> }> { + render() { + const { area } = this.props; + return ( + <Fragment> + {area.container.items.map((item) => item.content)} + </Fragment> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/theme.less b/packages/editor-skeleton/src/layouts/theme.less similarity index 100% rename from packages/vision-polyfill/src/skeleton/theme.less rename to packages/editor-skeleton/src/layouts/theme.less diff --git a/packages/editor-skeleton/src/layouts/toolbar.tsx b/packages/editor-skeleton/src/layouts/toolbar.tsx new file mode 100644 index 000000000..c266203c8 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/toolbar.tsx @@ -0,0 +1,49 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import Area from '../area'; + +@observer +export default class Toolbar extends Component<{ area: Area }> { + render() { + const { area } = this.props; + if (area.isEmpty()) { + return null; + } + return ( + <div + className={classNames('lc-toolbar', { + 'lc-area-visible': area.visible, + })} + > + <Contents area={area} /> + </div> + ); + } +} + +@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 ( + <Fragment> + <div className="lc-toolbar-left">{left}</div> + <div className="lc-toolbar-center">{center}</div> + <div className="lc-toolbar-right">{right}</div> + </Fragment> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/top-area.tsx b/packages/editor-skeleton/src/layouts/top-area.tsx new file mode 100644 index 000000000..532fc1fef --- /dev/null +++ b/packages/editor-skeleton/src/layouts/top-area.tsx @@ -0,0 +1,44 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@ali/lowcode-editor-core'; +import Area from '../area'; + +@observer +export default class TopArea extends Component<{ area: Area }> { + render() { + const { area } = this.props; + return ( + <div className={classNames("lc-top-area", { + 'lc-area-visible': area.visible + })}> + <Contents area={area} /> + </div> + ); + } +} + +@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') { + left.push(item.content); + } else { + right.push(item.content); + } + }); + return ( + <Fragment> + <div className="lc-top-area-left">{left}</div> + <div className="lc-top-area-center">{center}</div> + <div className="lc-top-area-right">{right}</div> + </Fragment> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/workbench.less b/packages/editor-skeleton/src/layouts/workbench.less similarity index 100% rename from packages/vision-polyfill/src/skeleton/workbench.less rename to packages/editor-skeleton/src/layouts/workbench.less diff --git a/packages/editor-skeleton/src/layouts/workbench.tsx b/packages/editor-skeleton/src/layouts/workbench.tsx new file mode 100644 index 000000000..857fb8540 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/workbench.tsx @@ -0,0 +1,40 @@ +import { Component } from 'react'; +import { TipContainer, observer } from '@ali/lowcode-editor-core'; +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 Workbench 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/editor-skeleton/src/locale/en-US.js b/packages/editor-skeleton/src/locale/en-US.js deleted file mode 100644 index ff8b4c563..000000000 --- a/packages/editor-skeleton/src/locale/en-US.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/packages/editor-skeleton/src/locale/ja-JP.js b/packages/editor-skeleton/src/locale/ja-JP.js deleted file mode 100644 index ff8b4c563..000000000 --- a/packages/editor-skeleton/src/locale/ja-JP.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/packages/editor-skeleton/src/locale/zh-CN.js b/packages/editor-skeleton/src/locale/zh-CN.js deleted file mode 100644 index ff8b4c563..000000000 --- a/packages/editor-skeleton/src/locale/zh-CN.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/packages/editor-skeleton/src/locale/zh-TW.js b/packages/editor-skeleton/src/locale/zh-TW.js deleted file mode 100644 index ff8b4c563..000000000 --- a/packages/editor-skeleton/src/locale/zh-TW.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/packages/editor-skeleton/src/panel.ts b/packages/editor-skeleton/src/panel.ts new file mode 100644 index 000000000..fd8a5de46 --- /dev/null +++ b/packages/editor-skeleton/src/panel.ts @@ -0,0 +1,166 @@ +import { createElement, ReactNode } from 'react'; +import { TitleContent } from '@ali/lowcode-types'; +import { uniqueId, createContent } from '@ali/lowcode-utils'; +import { obx } from '@ali/lowcode-editor-core'; +import WidgetContainer from './widget/widget-container'; +import { PanelConfig, HelpTipConfig } from './types'; +import { TitledPanelView, TabsPanelView, PanelView } from './components/widget-views'; +import { Skeleton } from './skeleton'; +import { composeTitle } from './widget/utils'; +import { IWidget } from './widget/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; + } + + readonly isPanel = true; + + private _body?: ReactNode; + get body() { + this.initBody(); + return this._body; + } + + get content(): ReactNode { + if (this.plain) { + return createElement(PanelView, { + panel: this, + key: this.id, + }); + } + return createElement(TitledPanelView, { panel: this, key: this.id }); + } + + readonly title: TitleContent; + readonly help?: HelpTipConfig; + private plain: boolean = false; + + private container?: WidgetContainer<Panel, PanelConfig>; + private parent?: WidgetContainer; + + constructor(readonly skeleton: Skeleton, readonly 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, + }); + } else { + const { content, contentProps } = this.config; + this._body = createContent(content, { + ...contentProps, + editor: this.skeleton.editor, + config: this.config, + panel: this, + }); + } + } + + 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; + } + + 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); + } + + hide() { + this.setActive(false); + } + + show() { + this.setActive(true); + } +} + +export function isPanel(obj: any): obj is Panel { + return obj && obj.isPanel; +} diff --git a/packages/editor-skeleton/src/skeleton.ts b/packages/editor-skeleton/src/skeleton.ts new file mode 100644 index 000000000..2dcf9470a --- /dev/null +++ b/packages/editor-skeleton/src/skeleton.ts @@ -0,0 +1,318 @@ +import { Editor } from '@ali/lowcode-editor-core'; +import { + DockConfig, + PanelConfig, + WidgetConfig, + IWidgetBaseConfig, + PanelDockConfig, + DialogDockConfig, + isDockConfig, + isPanelDockConfig, + isPanelConfig, + DividerConfig, + isDividerConfig +} from './types'; +import Panel, { isPanel } from './widget/panel'; +import WidgetContainer from './widget/widget-container'; +import Area from './area'; +import Widget, { isWidget, IWidget } from './widget/widget'; +import PanelDock from './widget/panel-dock'; +import Dock from './widget/dock'; +import { Stage, StageConfig } from './widget/stage'; +import { isValidElement } from 'react'; +import { isPlainObject } from '@ali/lowcode-utils'; +import { Divider } from '@alifd/next'; + +export enum SkeletonEvents { + PANEL_DOCK_ACTIVE = 'skeleton.panel-dock.active', + PANEL_DOCK_UNACTIVE = 'skeleton.panel-dock.unactive', + PANEL_SHOW = 'skeleton.panel.show', + PANEL_HIDE = 'skeleton.panel.hide', + WIDGET_SHOW = 'skeleton.widget.show', + WIDGET_HIDE = 'skeleton.widget.hide', +} + +export class Skeleton { + private panels = new Map<string, Panel>(); + private containers = new Map<string, WidgetContainer<any>>(); + readonly leftArea: Area<DockConfig | PanelDockConfig | DialogDockConfig>; + readonly topArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + readonly leftFixedArea: Area<PanelConfig, Panel>; + readonly leftFloatArea: Area<PanelConfig, Panel>; + readonly rightArea: Area<PanelConfig, Panel>; + readonly mainArea: Area<WidgetConfig | PanelConfig, Widget | Panel>; + readonly bottomArea: Area<PanelConfig, Panel>; + readonly stages: Area<StageConfig, Stage>; + + 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); + }); + + this.setupPlugins(); + } + + private setupPlugins() { + const { config, components: componentsMap } = this.editor; + const { plugins } = config; + if (!plugins) { + return; + } + Object.keys(plugins).forEach((area) => { + plugins[area].forEach((item) => { + const { pluginKey, type, props = {}, pluginProps } = item; + const config: any = { + area, + type: 'Widget', + name: pluginKey, + contentProps: pluginProps, + }; + const { dialogProps, balloonProps, panelProps, linkProps, ...restProps } = props; + config.props = restProps; + if (dialogProps) { + config.dialogProps = dialogProps; + } + if (balloonProps) { + config.balloonProps = balloonProps; + } + if (panelProps) { + config.panelProps = panelProps; + } + if (linkProps) { + config.linkProps = linkProps; + } + if (type === 'TabPanel') { + config.type = 'Panel'; + } else if (/Icon$/.test(type)) { + config.type = type.replace('Icon', 'Dock'); + } + if (pluginKey in componentsMap) { + config.content = componentsMap[pluginKey]; + } + this.add(config); + }); + }); + } + + postEvent(event: SkeletonEvents, ...args: any[]) { + this.editor.emit(event, ...args); + } + + createWidget(config: IWidgetBaseConfig | IWidget) { + if (isWidget(config)) { + return config; + } + + config = this.parseConfig(config); + let widget: IWidget; + if (isDockConfig(config)) { + if (isPanelDockConfig(config)) { + widget = new PanelDock(this, config); + } else if (false) { + // DialogDock + // others... + } else { + + widget = new Dock(this, config); + } + } else if (isDividerConfig(config)) { + widget = new Widget(this, { + ...config, + type: 'Widget', + content: Divider, + }); + } else if (isPanelConfig(config)) { + widget = this.createPanel(config); + } else { + widget = new Widget(this, config as WidgetConfig); + } + return widget; + } + + createPanel(config: PanelConfig) { + config = this.parseConfig(config); + 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 = false, + checkVisible: () => boolean = () => true, + defaultSetCurrent = false, + ) { + const container = new WidgetContainer(name, handle, exclusive, checkVisible, defaultSetCurrent); + this.containers.set(name, container); + return container; + } + + private parseConfig(config: IWidgetBaseConfig): any { + if ((config as any).parsed) { + return config; + } + const { content, ...restConfig } = config; + if (content) { + if (isPlainObject(content) && !isValidElement(content)) { + Object.keys(content).forEach((key) => { + if (/props$/i.test(key) && restConfig[key]) { + restConfig[key] = { + ...restConfig[key], + ...content[key], + }; + } else { + restConfig[key] = content[key]; + } + }); + } else { + restConfig.content = content; + } + } + restConfig.pluginKey = restConfig.name; + restConfig.parsed = true; + return restConfig; + } + + add(config: IWidgetBaseConfig & { area?: string }, extraConfig?: object) { + const parsedConfig: any = { + ...this.parseConfig(config), + ...extraConfig, + }; + let { area } = parsedConfig; + if (!area) { + if (parsedConfig.type === 'Panel') { + area = 'leftFloatArea' + } else if (parsedConfig.type === 'Widget') { + area = 'mainArea'; + } else { + area = 'leftArea'; + } + } + switch (area) { + case 'leftArea': + case 'left': + return this.leftArea.add(parsedConfig); + case 'rightArea': + case 'right': + return this.rightArea.add(parsedConfig); + case 'topArea': + case 'top': + return this.topArea.add(parsedConfig); + case 'toolbar': + return this.toolbar.add(parsedConfig); + case 'mainArea': + case 'main': + case 'center': + case 'centerArea': + return this.mainArea.add(parsedConfig); + case 'bottomArea': + case 'bottom': + return this.bottomArea.add(parsedConfig); + case 'leftFixedArea': + return this.leftFixedArea.add(parsedConfig); + case 'leftFloatArea': + return this.leftFloatArea.add(parsedConfig); + case 'stages': + return this.stages.add(parsedConfig); + } + } +} diff --git a/packages/editor-skeleton/src/skeleton.tsx b/packages/editor-skeleton/src/skeleton.tsx deleted file mode 100644 index ddcd842a0..000000000 --- a/packages/editor-skeleton/src/skeleton.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { Loading, ConfigProvider } from '@alifd/next'; -import { HashRouter as Router, Route } from 'react-router-dom'; -import Editor, { utils } from '@ali/lowcode-editor-core'; -import { - EditorConfig, - Utils, - PluginClassSet, -} from '@ali/lowcode-editor-core/lib/definitions'; -import defaultConfig from './config/skeleton'; -import skeletonUtils from './config/utils'; - -import TopArea from './layouts/TopArea'; -import LeftArea from './layouts/LeftArea'; -import CenterArea from './layouts/CenterArea'; -import RightArea from './layouts/RightArea'; -import './global.scss'; - -const { comboEditorConfig, parseSearch } = utils; - -let renderIdx = 0; - -declare global { - interface Window { - __ctx: { - editor: Editor; - appHelper: Editor; - }; - } -} - -export interface SkeletonProps { - components?: PluginClassSet; - config?: EditorConfig; - history?: object; - location?: object; - match?: object; - utils?: Utils; - editor?: Editor; -} - -export interface SkeletonState { - initReady?: boolean; - skeletonKey?: string; - __hasError?: boolean; -} - -export class Skeleton extends PureComponent<SkeletonProps, SkeletonState> { - static displayName = 'LowcodeEditorSkeleton'; - - static getDerivedStateFromError(): SkeletonState { - return { - __hasError: true, - }; - } - - private editor: Editor; - - constructor(props) { - super(props); - - this.state = { - initReady: false, - skeletonKey: `skeleton${renderIdx}`, - }; - - this.init(); - } - - componentWillUnmount(): void { - this.editor && this.editor.destroy(); - } - - componentDidCatch(err): void { - console.error(err); - } - - init = (isReset: boolean = false): void => { - if (this.editor) { - this.editor.destroy(); - } - const { utils, config, components, editor } = this.props; - if (!editor) { - this.editor = new Editor( - comboEditorConfig(defaultConfig, config), - components, - { - ...skeletonUtils, - ...utils, - }, - ); - this.editor.once('editor.reset', (): void => { - this.setState({ - initReady: false, - }); - this.editor.emit('editor.beforeReset'); - this.init(true); - }); - } else { - this.editor = editor; - } - - // eslint-disable-next-line no-underscore-dangle - window.__ctx = { - editor: this.editor, - appHelper: this.editor, - }; - this.editor.init().then((): void => { - this.setState( - { - initReady: true, - // 刷新IDE时生成新的skeletonKey保证插件生命周期重新执行 - skeletonKey: isReset - ? `skeleton${++renderIdx}` - : this.state.skeletonKey, - }, - (): void => { - editor.emit('editor.ready'); - isReset && editor.emit('editor.afterReset'); - }, - ); - }); - }; - - render(): React.ReactNode { - const { initReady, skeletonKey, __hasError } = this.state; - const { location, history, match } = this.props; - if (__hasError || !this.editor) { - return 'error'; - } - - location.query = parseSearch(location.search); - this.editor.set('location', location); - this.editor.set('history', history); - this.editor.set('match', match); - - return ( - <ConfigProvider> - <Loading tip="Loading" size="large" visible={!initReady} fullScreen> - <div className="lowcode-editor" key={skeletonKey}> - <TopArea editor={this.editor} /> - <div className="lowcode-main-content"> - <LeftArea.Nav editor={this.editor} /> - <LeftArea.Panel editor={this.editor} /> - <CenterArea editor={this.editor} /> - <RightArea editor={this.editor} /> - </div> - </div> - </Loading> - </ConfigProvider> - ); - } -} - -// 通过React-Router包裹,支持编辑器内页面根据路由切换 -export interface SkeletonWithRouterProps { - components?: PluginClassSet; - config?: EditorConfig; - utils?: Utils; - editor?: Editor; -} - -const SkeletonWithRouter: React.FC<SkeletonWithRouterProps> = ( - props, -): React.ReactElement => { - const { config, ...otherProps } = props; - return ( - <Router> - <Route - path="/*" - component={routerProps => ( - <Skeleton - {...routerProps} - {...otherProps} - {...(config && config.skeleton && config.skeleton.props)} - config={config} - /> - )} - /> - </Router> - ); -}; - -export default SkeletonWithRouter; diff --git a/packages/editor-skeleton/src/types.ts b/packages/editor-skeleton/src/types.ts new file mode 100644 index 000000000..09c669985 --- /dev/null +++ b/packages/editor-skeleton/src/types.ts @@ -0,0 +1,114 @@ +import { ReactElement, ComponentType } from 'react'; +import { TitleContent, IconType, I18nData, TipContent } from '@ali/lowcode-types'; + +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"; + props?: { + align?: "left" | "right" | "bottom" | "center" | "top"; + }; + content?: string | ReactElement | ComponentType<any>; // children +} + +export function isWidgetConfig(obj: any): obj is WidgetConfig { + return obj && obj.type === "Widget"; +} + +export interface DockProps { + title?: TitleContent; + icon?: IconType; + size?: 'small' | 'medium' | 'large'; + className?: string; + description?: TipContent; + onClick?: () => void; +} + +export interface DividerConfig extends IWidgetBaseConfig { + type: "Divider"; + props?: { + align?: "left" | "right" | "center"; + }; +} + +export function isDividerConfig(obj: any): obj is DividerConfig { + return obj && obj.type === "Divider"; +} + +export interface IDockBaseConfig extends IWidgetBaseConfig { + props?: DockProps & { + align?: "left" | "right" | "bottom" | "center" | "top"; + }; +} + +export interface DockConfig extends IDockBaseConfig { + type: "Dock"; + content?: string | ReactElement | ComponentType<any>; +} + +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<any> | 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<any> | 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/dialog-dock.ts b/packages/editor-skeleton/src/widget/dialog-dock.ts similarity index 100% rename from packages/vision-polyfill/src/skeleton/dialog-dock.ts rename to packages/editor-skeleton/src/widget/dialog-dock.ts diff --git a/packages/editor-skeleton/src/widget/dock.ts b/packages/editor-skeleton/src/widget/dock.ts new file mode 100644 index 000000000..c63bbdc2c --- /dev/null +++ b/packages/editor-skeleton/src/widget/dock.ts @@ -0,0 +1,87 @@ +import { ReactNode, createElement } from 'react'; +import { obx } from '@ali/lowcode-editor-core'; +import { uniqueId, createContent } from '@ali/lowcode-utils'; +import { DockConfig } from '../types'; +import { Skeleton } from '../skeleton'; +import { DockView, WidgetView } from '../components/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; + } + + get content(): ReactNode { + return createElement(WidgetView, { + widget: this, + key: this.id, + }); + } + + private inited: boolean = false; + private _body: ReactNode; + get body() { + if (this.inited) { + return this._body; + } + + const { props, content, contentProps } = this.config; + + if (content) { + this._body = createContent(content, { + ...contentProps, + config: this.config, + editor: this.skeleton.editor, + }); + } else { + this._body = createElement(DockView, props); + } + return this._body; + } + + constructor(readonly skeleton: Skeleton, readonly 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); + } + + toggle() { + this.setVisible(!this._visible); + } +} diff --git a/packages/editor-skeleton/src/widget/panel-dock.ts b/packages/editor-skeleton/src/widget/panel-dock.ts new file mode 100644 index 000000000..7b06882fe --- /dev/null +++ b/packages/editor-skeleton/src/widget/panel-dock.ts @@ -0,0 +1,118 @@ +import { obx, computed } from '@ali/lowcode-editor-core'; +import { uniqueId } from '@ali/lowcode-utils'; +import { createElement, ReactNode } from 'react'; +import { Skeleton } from '../skeleton'; +import { PanelDockConfig } from '../types'; +import Panel from './panel'; +import { PanelDockView, WidgetView } from '../components/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 _body: ReactNode; + get body() { + if (this.inited) { + return this._body; + } + this.inited = true; + const { props } = this.config; + + this._body = createElement(PanelDockView, { + ...props, + dock: this, + }); + + return this._body; + } + + get content(): ReactNode { + return createElement(WidgetView, { + widget: this, + key: this.id, + }); + } + + @obx.ref private _visible: boolean = true; + get visible() { + return this._visible; + } + + @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, readonly config: PanelDockConfig) { + const { content, contentProps, panelProps, name, props } = 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: { + // FIXME! give default title for panel + // title: props ? composeTitle(props?.title, props?.icon, props?.description, true) : '', + ...panelProps, + }, + contentProps, + content, + area: panelProps?.area + }) as Panel; + } + } + + setVisible(flag: boolean) { + if (flag === this._visible) { + return; + } + if (flag) { + this._visible = true; + } else if (this.inited) { + this._visible = false; + } + } + + hide() { + this.setVisible(false); + } + + show() { + this.setVisible(true); + } + + toggle() { + this.setVisible(!this._visible); + } + + togglePanel() { + this.panel?.toggle(); + } + + getName() { + return this.name; + } + + getContent() { + return this.content; + } + + hidePanel() { + this.panel?.setActive(false); + } + + showPanel() { + this.panel?.setActive(true); + } +} diff --git a/packages/editor-skeleton/src/widget/panel.ts b/packages/editor-skeleton/src/widget/panel.ts new file mode 100644 index 000000000..f75e69a3d --- /dev/null +++ b/packages/editor-skeleton/src/widget/panel.ts @@ -0,0 +1,166 @@ +import { createElement, ReactNode } from 'react'; +import { obx } from '@ali/lowcode-editor-core'; +import { uniqueId, createContent } from '@ali/lowcode-utils'; +import { TitleContent } from '@ali/lowcode-types'; +import WidgetContainer from './widget-container'; +import { PanelConfig, HelpTipConfig } from '../types'; +import { TitledPanelView, TabsPanelView, PanelView } from '../components/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; + } + + readonly isPanel = true; + + private _body?: ReactNode; + get body() { + this.initBody(); + return this._body; + } + + get content(): ReactNode { + if (this.plain) { + return createElement(PanelView, { + panel: this, + key: this.id, + }); + } + return createElement(TitledPanelView, { panel: this, key: this.id }); + } + + readonly title: TitleContent; + readonly help?: HelpTipConfig; + private plain: boolean = false; + + private container?: WidgetContainer<Panel, PanelConfig>; + private parent?: WidgetContainer; + + constructor(readonly skeleton: Skeleton, readonly 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, + }); + } else { + const { content, contentProps } = this.config; + this._body = createContent(content, { + ...contentProps, + editor: this.skeleton.editor, + config: this.config, + panel: this, + }); + } + } + + 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; + } + + 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); + } + + 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/stage.ts b/packages/editor-skeleton/src/widget/stage.ts similarity index 92% rename from packages/vision-polyfill/src/skeleton/stage.ts rename to packages/editor-skeleton/src/widget/stage.ts index f5c59a557..a4bf483f2 100644 --- a/packages/vision-polyfill/src/skeleton/stage.ts +++ b/packages/editor-skeleton/src/widget/stage.ts @@ -1,6 +1,6 @@ import Widget from './widget'; -import { Skeleton } from './skeleton'; -import { WidgetConfig } from './types'; +import { Skeleton } from '../skeleton'; +import { WidgetConfig } from '../types'; export interface StageConfig extends WidgetConfig { isRoot?: boolean; diff --git a/packages/vision-polyfill/src/skeleton/utils.ts b/packages/editor-skeleton/src/widget/utils.ts similarity index 100% rename from packages/vision-polyfill/src/skeleton/utils.ts rename to packages/editor-skeleton/src/widget/utils.ts diff --git a/packages/vision-polyfill/src/skeleton/widget-container.ts b/packages/editor-skeleton/src/widget/widget-container.ts similarity index 100% rename from packages/vision-polyfill/src/skeleton/widget-container.ts rename to packages/editor-skeleton/src/widget/widget-container.ts diff --git a/packages/editor-skeleton/src/widget/widget.ts b/packages/editor-skeleton/src/widget/widget.ts new file mode 100644 index 000000000..91bd90344 --- /dev/null +++ b/packages/editor-skeleton/src/widget/widget.ts @@ -0,0 +1,101 @@ +import { ReactNode, createElement } from 'react'; +import { obx } from '@ali/lowcode-editor-core'; +import { createContent, uniqueId } from '@ali/lowcode-utils'; +import { WidgetConfig, IWidgetBaseConfig } from '../types'; +import { Skeleton } from '../skeleton'; +import { WidgetView } from '../components/widget-views'; + +export interface IWidget { + readonly name: string; + readonly content: ReactNode; + readonly align?: string; + readonly isWidget: true; + readonly visible: boolean; + readonly body: ReactNode; + readonly skeleton: Skeleton; + readonly config: IWidgetBaseConfig; + + getName(): string; + getContent(): any; + show(): void; + hide(): void; + toggle(): 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, + config: this.config, + editor: this.skeleton.editor, + }); + return this._body; + } + + get content(): ReactNode { + return createElement(WidgetView, { + widget: this, + key: this.id, + }); + } + + constructor(readonly skeleton: Skeleton, readonly 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/globals/CHANGELOG.md b/packages/globals/CHANGELOG.md deleted file mode 100644 index 74216bad2..000000000 --- a/packages/globals/CHANGELOG.md +++ /dev/null @@ -1,78 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -<a name="0.9.3"></a> -## [0.9.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-globals@0.9.2...@ali/lowcode-globals@0.9.3) (2020-04-16) - - - - -**Note:** Version bump only for package @ali/lowcode-globals - -<a name="0.9.2"></a> -## [0.9.2](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-globals@0.9.1...@ali/lowcode-globals@0.9.2) (2020-04-15) - - - - -**Note:** Version bump only for package @ali/lowcode-globals - -<a name="0.9.1"></a> -## [0.9.1](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-globals@0.9.0...@ali/lowcode-globals@0.9.1) (2020-03-31) - - - - -**Note:** Version bump only for package @ali/lowcode-globals - -<a name="0.9.0"></a> -# [0.9.0](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-globals@0.8.4...@ali/lowcode-globals@0.9.0) (2020-03-30) - - -### Features - -* **designer:** add builtin hotkeys ([2ec5883](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/2ec5883)) - - - - -<a name="0.8.4"></a> -## [0.8.4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-globals@0.8.3...@ali/lowcode-globals@0.8.4) (2020-03-30) - - - - -**Note:** Version bump only for package @ali/lowcode-globals - -<a name="0.8.3"></a> -## [0.8.3](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-globals@0.8.2...@ali/lowcode-globals@0.8.3) (2020-03-30) - - - - -**Note:** Version bump only for package @ali/lowcode-globals - -<a name="0.8.2"></a> -## 0.8.2 (2020-03-30) - - -### Features - -* double outline & ZH_EN support ([b379bd7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b379bd7)) - - - - -<a name="0.8.1"></a> -## 0.8.1 (2020-03-30) - - -### Features - -<<<<<<< HEAD -* double outline & ZH_EN support ([b379bd7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b379bd7)) -======= -* double outline & ZH_EN support ([b379bd7](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/b379bd7c0c488ef24f825760750a13d3fa083c96)) ->>>>>>> df955e1db90ff104cd11160def80113cfd6faccc diff --git a/packages/globals/README.md b/packages/globals/README.md deleted file mode 100644 index 30c3e7d7d..000000000 --- a/packages/globals/README.md +++ /dev/null @@ -1,3 +0,0 @@ -shared globals - - 发 CDN diff --git a/packages/globals/build.json b/packages/globals/build.json deleted file mode 100644 index e791d5b6b..000000000 --- a/packages/globals/build.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plugins": [ - "build-plugin-component", - "build-plugin-fusion", - ["build-plugin-moment-locales", { - "locales": ["zh-cn"] - }] - ] -} diff --git a/packages/globals/src/components/tip/embed-tip.tsx b/packages/globals/src/components/tip/embed-tip.tsx deleted file mode 100644 index 290cf2f9a..000000000 --- a/packages/globals/src/components/tip/embed-tip.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { uniqueId } from '../../utils'; -import { Component } from 'react'; -import { saveTips } from './tip-handler'; -import { TipConfig } from '../../types'; - -export default class EmbedTip extends Component<TipConfig> { - private id = uniqueId('tips$'); - - componentWillUnmount() { - saveTips(this.id, null); - } - - render() { - saveTips(this.id, this.props); - return <meta data-role="tip" data-tip-id={this.id} />; - } -} diff --git a/packages/globals/src/components/tip/index.ts b/packages/globals/src/components/tip/index.ts deleted file mode 100644 index ea80c70ac..000000000 --- a/packages/globals/src/components/tip/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './style.less'; - -export { default as EmbedTip } from './embed-tip'; -export { default as TipContainer } from './tip-container'; diff --git a/packages/globals/src/di/editor.ts b/packages/globals/src/di/editor.ts deleted file mode 100644 index f3f7f2214..000000000 --- a/packages/globals/src/di/editor.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { EventEmitter } from 'events'; -import { RegisterOptions } from 'power-di'; - -export type KeyType = Function | symbol | string; -export type ClassType = Function | (new (...args: any[]) => any); -export interface GetOptions { - forceNew?: boolean; - sourceCls?: ClassType; -} -export type GetReturnType<T, ClsType> = T extends undefined - ? ClsType extends { - prototype: infer R; - } - ? R - : any - : T; - -export interface IEditor extends EventEmitter { - get<T = undefined, KeyOrType = any>(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType<T, KeyOrType> | undefined; - - has(keyOrType: KeyType): boolean; - - set(key: KeyType, data: any): void; - - onceGot<T = undefined, KeyOrType extends KeyType = any>(keyOrType: KeyOrType): Promise<GetReturnType<T, KeyOrType>>; - - onGot<T = undefined, KeyOrType extends KeyType = any>( - keyOrType: KeyOrType, - fn: (data: GetReturnType<T, KeyOrType>) => void, - ): () => void; - - register(data: any, key?: KeyType, options?: RegisterOptions): void; -} diff --git a/packages/globals/src/di/transducer.ts b/packages/globals/src/di/transducer.ts deleted file mode 100644 index 93e079bf7..000000000 --- a/packages/globals/src/di/transducer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TransformedComponentMetadata } from '../types'; - -export interface MetadataTransducer { - (prev: TransformedComponentMetadata): TransformedComponentMetadata; - /** - * 0 - 9 system - * 10 - 99 builtin-plugin - * 100 - app & plugin - */ - level?: number; - /** - * use to replace TODO - */ - id?: string; -} -const metadataTransducers: MetadataTransducer[] = []; - -export function registerMetadataTransducer(transducer: MetadataTransducer, level: number = 100, id?: string) { - transducer.level = level; - transducer.id = id; - const i = metadataTransducers.findIndex(item => item.level != null && item.level > level); - if (i < 0) { - metadataTransducers.push(transducer); - } else { - metadataTransducers.splice(i, 0, transducer); - } -} - -export function getRegisteredMetadataTransducers(): MetadataTransducer[] { - return metadataTransducers; -} diff --git a/packages/globals/src/index.ts b/packages/globals/src/index.ts deleted file mode 100644 index 51d80c5da..000000000 --- a/packages/globals/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './intl'; -export * from './components'; -export * from './utils'; -export * from './types'; -export * from './di'; -export * from './obx'; diff --git a/packages/plugin-components-pane/src/index.tsx b/packages/plugin-components-pane/src/index.tsx index 24581199f..3dc8ccd5a 100644 --- a/packages/plugin-components-pane/src/index.tsx +++ b/packages/plugin-components-pane/src/index.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { Icon, Search, Select } from '@alifd/next'; import MaterialShow from '@ali/iceluna-comp-material-show'; -import { PluginProps } from '@ali/lowcode-editor-core'; +import { PluginProps } from '@ali/lowcode-types'; import { Designer } from '@ali/lowcode-designer'; import './index.scss'; diff --git a/packages/plugin-event-bind-dialog/src/index.tsx b/packages/plugin-event-bind-dialog/src/index.tsx index 32cf6e474..4aedba085 100644 --- a/packages/plugin-event-bind-dialog/src/index.tsx +++ b/packages/plugin-event-bind-dialog/src/index.tsx @@ -1,6 +1,6 @@ import { Component, isValidElement, ReactElement, ReactNode } from 'react'; import { Dialog, Search, Input } from '@alifd/next'; -import Editor from '@ali/lowcode-editor-core'; +import { Editor } from '@ali/lowcode-editor-core'; import './index.scss'; export default class EventBindDialog extends Component<{ diff --git a/packages/plugin-outline-pane/src/helper/indent-track.ts b/packages/plugin-outline-pane/src/helper/indent-track.ts index 373b59931..37ed540b8 100644 --- a/packages/plugin-outline-pane/src/helper/indent-track.ts +++ b/packages/plugin-outline-pane/src/helper/indent-track.ts @@ -1,4 +1,4 @@ -import { DropLocation, ParentalNode, isLocationChildrenDetail } from '@ali/lowcode-designer'; +import { DropLocation, ParentalNode, isLocationChildrenDetail, Node } from '@ali/lowcode-designer'; const IndentSensitive = 15; export class IndentTrack { @@ -33,10 +33,10 @@ export class IndentTrack { const index = loc.detail.index; if (direction === 'left') { - if (parent.isSlot() || !parent.parent || index < parent.children.size) { + if (!parent.parent || index < parent.children.size || parent.isSlot()) { return null; } - return [parent.parent, parent.index + 1]; + return [(parent as any).parent, parent.index + 1]; } else { if (index === 0) { return null; diff --git a/packages/plugin-outline-pane/src/icons/arrow-right.tsx b/packages/plugin-outline-pane/src/icons/arrow-right.tsx index 78772d656..1f4562606 100644 --- a/packages/plugin-outline-pane/src/icons/arrow-right.tsx +++ b/packages/plugin-outline-pane/src/icons/arrow-right.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconArrowRight(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/cond.tsx b/packages/plugin-outline-pane/src/icons/cond.tsx index 53ef29295..915efebec 100644 --- a/packages/plugin-outline-pane/src/icons/cond.tsx +++ b/packages/plugin-outline-pane/src/icons/cond.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconCond(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/eye-close.tsx b/packages/plugin-outline-pane/src/icons/eye-close.tsx index 7eb56129e..4502d1f93 100644 --- a/packages/plugin-outline-pane/src/icons/eye-close.tsx +++ b/packages/plugin-outline-pane/src/icons/eye-close.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconEyeClose(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/eye.tsx b/packages/plugin-outline-pane/src/icons/eye.tsx index aa2f16cd5..5490e11a5 100644 --- a/packages/plugin-outline-pane/src/icons/eye.tsx +++ b/packages/plugin-outline-pane/src/icons/eye.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconEye(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/lock.tsx b/packages/plugin-outline-pane/src/icons/lock.tsx index e123741d1..7a12db4a7 100644 --- a/packages/plugin-outline-pane/src/icons/lock.tsx +++ b/packages/plugin-outline-pane/src/icons/lock.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconLock(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/loop.tsx b/packages/plugin-outline-pane/src/icons/loop.tsx index 85b184a32..ba86e1c86 100644 --- a/packages/plugin-outline-pane/src/icons/loop.tsx +++ b/packages/plugin-outline-pane/src/icons/loop.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconLoop(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/outline.tsx b/packages/plugin-outline-pane/src/icons/outline.tsx index 29e0d111b..78658c867 100644 --- a/packages/plugin-outline-pane/src/icons/outline.tsx +++ b/packages/plugin-outline-pane/src/icons/outline.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconOutline(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/slot.tsx b/packages/plugin-outline-pane/src/icons/slot.tsx index 973bd3ecd..ed27e74c7 100644 --- a/packages/plugin-outline-pane/src/icons/slot.tsx +++ b/packages/plugin-outline-pane/src/icons/slot.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconSlot(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/icons/unlock.tsx b/packages/plugin-outline-pane/src/icons/unlock.tsx index 5384822fd..5695db95d 100644 --- a/packages/plugin-outline-pane/src/icons/unlock.tsx +++ b/packages/plugin-outline-pane/src/icons/unlock.tsx @@ -1,4 +1,4 @@ -import { SVGIcon, IconProps } from '@ali/lowcode-globals'; +import { SVGIcon, IconProps } from '@ali/lowcode-utils'; export function IconUnlock(props: IconProps) { return ( diff --git a/packages/plugin-outline-pane/src/locale/index.ts b/packages/plugin-outline-pane/src/locale/index.ts index 913dd42f8..dfd17521f 100644 --- a/packages/plugin-outline-pane/src/locale/index.ts +++ b/packages/plugin-outline-pane/src/locale/index.ts @@ -1,4 +1,4 @@ -import { createIntl } from '@ali/lowcode-globals'; +import { createIntl } from '@ali/lowcode-editor-core'; import en_US from './en-US.json'; import zh_CN from './zh-CN.json'; diff --git a/packages/plugin-outline-pane/src/main.ts b/packages/plugin-outline-pane/src/main.ts index c3c94ce0f..3903f166d 100644 --- a/packages/plugin-outline-pane/src/main.ts +++ b/packages/plugin-outline-pane/src/main.ts @@ -1,4 +1,5 @@ -import { computed, obx, uniqueId } from '@ali/lowcode-globals'; +import { EventEmitter } from 'events'; +import { computed, obx, Editor } from '@ali/lowcode-editor-core'; import { Designer, ISensor, @@ -17,12 +18,11 @@ import { contains, Node, } from '@ali/lowcode-designer'; -import { Editor } from '@ali/lowcode-editor-core'; import { Tree } from './tree'; import TreeNode from './tree-node'; import { IndentTrack } from './helper/indent-track'; import DwellTimer from './helper/dwell-timer'; -import { EventEmitter } from 'events'; +import { uniqueId } from '@ali/lowcode-utils'; export interface IScrollBoard { scrollToNode(treeNode: TreeNode, detail?: any): void; diff --git a/packages/plugin-outline-pane/src/tree-node.ts b/packages/plugin-outline-pane/src/tree-node.ts index c7d5f24d5..e80a51146 100644 --- a/packages/plugin-outline-pane/src/tree-node.ts +++ b/packages/plugin-outline-pane/src/tree-node.ts @@ -1,4 +1,5 @@ -import { computed, obx, TitleContent, isI18nData, localeFormat } from '@ali/lowcode-globals'; +import { TitleContent, isI18nData } from '@ali/lowcode-types'; +import { computed, obx, intl } from '@ali/lowcode-editor-core'; import { Node, DocumentModel, isLocationChildrenDetail, LocationChildrenDetail, Designer } from '@ali/lowcode-designer'; import { Tree } from './tree'; @@ -121,7 +122,7 @@ export default class TreeNode { return title; } if (isI18nData(title)) { - return localeFormat(title); + return intl(title); } return this.node.componentName; } diff --git a/packages/plugin-outline-pane/src/views/pane.tsx b/packages/plugin-outline-pane/src/views/pane.tsx index d18e1cfef..f539b51be 100644 --- a/packages/plugin-outline-pane/src/views/pane.tsx +++ b/packages/plugin-outline-pane/src/views/pane.tsx @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { observer } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; import { intl } from '../locale'; import { OutlineMain } from '../main'; import TreeView from './tree'; diff --git a/packages/plugin-outline-pane/src/views/tree-branches.tsx b/packages/plugin-outline-pane/src/views/tree-branches.tsx index 6193652d0..de8308514 100644 --- a/packages/plugin-outline-pane/src/views/tree-branches.tsx +++ b/packages/plugin-outline-pane/src/views/tree-branches.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; import classNames from 'classnames'; -import { observer, Title } from '@ali/lowcode-globals'; +import { observer, Title } from '@ali/lowcode-editor-core'; import { ExclusiveGroup } from '@ali/lowcode-designer'; import TreeNode from '../tree-node'; import TreeNodeView from './tree-node'; diff --git a/packages/plugin-outline-pane/src/views/tree-node.tsx b/packages/plugin-outline-pane/src/views/tree-node.tsx index b99d2201e..7688b67c6 100644 --- a/packages/plugin-outline-pane/src/views/tree-node.tsx +++ b/packages/plugin-outline-pane/src/views/tree-node.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; import classNames from 'classnames'; -import { observer } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; import TreeNode from '../tree-node'; import TreeTitle from './tree-title'; import TreeBranches from './tree-branches'; diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx index 516314f3c..3d7c6b373 100644 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -1,6 +1,6 @@ import { Component, KeyboardEvent, FocusEvent, Fragment } from 'react'; import classNames from 'classnames'; -import { observer, createIcon, Title, EmbedTip } from '@ali/lowcode-globals'; +import { observer, Title, Tip } from '@ali/lowcode-editor-core'; import { IconArrowRight } from '../icons/arrow-right'; import { IconEyeClose } from '../icons/eye-close'; import { IconLock } from '../icons/lock'; @@ -11,6 +11,7 @@ import { IconEye } from '../icons/eye'; import { IconCond } from '../icons/cond'; import { IconLoop } from '../icons/loop'; import { IconSlot } from '../icons/slot'; +import { createIcon } from '@ali/lowcode-utils'; @observer export default class TreeTitle extends Component<{ @@ -104,21 +105,21 @@ export default class TreeTitle extends Component<{ <a className="tree-node-tag slot"> {/* todo: click redirect to prop */} <IconSlot /> - <EmbedTip>{intl('Slot for {prop}', { prop: node.slotFor.key })}</EmbedTip> + <Tip>{intl('Slot for {prop}', { prop: node.slotFor.key })}</Tip> </a> )} {node.hasLoop() && ( <a className="tree-node-tag loop"> {/* todo: click todo something */} <IconLoop /> - <EmbedTip>{intl('Loop')}</EmbedTip> + <Tip>{intl('Loop')}</Tip> </a> )} {node.hasCondition() && !node.conditionGroup && ( <a className="tree-node-tag cond"> {/* todo: click todo something */} <IconCond /> - <EmbedTip>{intl('Conditional')}</EmbedTip> + <Tip>{intl('Conditional')}</Tip> </a> )} </Fragment> @@ -147,7 +148,7 @@ class LockBtn extends Component<{ treeNode: TreeNode }> { }} > {treeNode.locked ? <IconLock /> : <IconUnlock />} - <EmbedTip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</EmbedTip> + <Tip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</Tip> </div> ); } @@ -169,7 +170,7 @@ class HideBtn extends Component<{ treeNode: TreeNode }> { }} > {treeNode.hidden ? <IconEyeClose /> : <IconEye />} - <EmbedTip>{treeNode.hidden ? intl('Show') : intl('Hide')}</EmbedTip> + <Tip>{treeNode.hidden ? intl('Show') : intl('Hide')}</Tip> </div> ); } diff --git a/packages/plugin-outline-pane/src/views/tree.tsx b/packages/plugin-outline-pane/src/views/tree.tsx index 2ebce693e..04d98c496 100644 --- a/packages/plugin-outline-pane/src/views/tree.tsx +++ b/packages/plugin-outline-pane/src/views/tree.tsx @@ -1,8 +1,9 @@ import { Component, MouseEvent as ReactMouseEvent } from 'react'; -import { observer, isFormEvent } from '@ali/lowcode-globals'; +import { observer } from '@ali/lowcode-editor-core'; +import { isRootNode, Node, DragObjectType, isShaken } from '@ali/lowcode-designer'; +import { isFormEvent } from '@ali/lowcode-utils'; import { Tree } from '../tree'; import TreeNodeView from './tree-node'; -import { isRootNode, Node, DragObjectType, isShaken } from '@ali/lowcode-designer'; function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string { let target: Element | null = e.target as Element; diff --git a/packages/plugin-sample-logo/src/index.tsx b/packages/plugin-sample-logo/src/index.tsx index fdd780e9c..4170481fc 100644 --- a/packages/plugin-sample-logo/src/index.tsx +++ b/packages/plugin-sample-logo/src/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import './index.scss'; -import { PluginProps } from '@ali/lowcode-editor-core'; +import { PluginProps } from '@ali/lowcode-types'; export interface IProps { logo?: string; diff --git a/packages/plugin-sample-preview/src/index.tsx b/packages/plugin-sample-preview/src/index.tsx index cac06d8ca..df10c5466 100644 --- a/packages/plugin-sample-preview/src/index.tsx +++ b/packages/plugin-sample-preview/src/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from '@alifd/next'; -import { PluginProps } from '@ali/lowcode-editor-core'; +import { PluginProps } from '@ali/lowcode-types'; import { Designer } from '@ali/lowcode-designer'; import './index.scss'; diff --git a/packages/plugin-source-editor/src/index.tsx b/packages/plugin-source-editor/src/index.tsx index e90f30e51..9039c2a67 100644 --- a/packages/plugin-source-editor/src/index.tsx +++ b/packages/plugin-source-editor/src/index.tsx @@ -3,7 +3,7 @@ import { Tab, Search, Input, Button } from '@alifd/next'; import Editor from '@ali/lowcode-editor-core'; import { js_beautify, css_beautify } from 'js-beautify'; import MonacoEditor from 'react-monaco-editor'; -import Panel from '../../vision-polyfill/src/skeleton/panel'; +import Panel from '../../vision-polyfill/src/skeleton/widget/panel'; // import lolizer from './sorceEditorPlugin', diff --git a/packages/plugin-variable-bind-dialog/src/index.tsx b/packages/plugin-variable-bind-dialog/src/index.tsx index 4beded5bd..75ee084bd 100644 --- a/packages/plugin-variable-bind-dialog/src/index.tsx +++ b/packages/plugin-variable-bind-dialog/src/index.tsx @@ -1,11 +1,9 @@ -import { Component, isValidElement, ReactElement, ReactNode } from 'react'; -import { Dialog, Search, Input ,Button} from '@alifd/next'; -import Editor from '@ali/lowcode-editor-core'; +import { Component } from 'react'; +import { Dialog, Input, Button } from '@alifd/next'; +import { PluginProps } from '@ali/lowcode-types'; import './index.scss'; -export default class VariableBindDialog extends Component<{ - editor: Editor; -}> { +export default class VariableBindDialog extends Component<PluginProps> { private loopVariableList: any[] = [ { name: 'item', diff --git a/packages/plugin-zh-en/src/index.tsx b/packages/plugin-zh-en/src/index.tsx index 7c44ae362..eebbe37b8 100644 --- a/packages/plugin-zh-en/src/index.tsx +++ b/packages/plugin-zh-en/src/index.tsx @@ -1,6 +1,6 @@ import { PureComponent } from 'react'; -import { globalLocale, EmbedTip } from '@ali/lowcode-globals'; -import { PluginProps } from '@ali/lowcode-editor-core'; +import { globalLocale, Tip } from '@ali/lowcode-editor-core'; +import { PluginProps } from '@ali/lowcode-types'; import { intl } from './locale'; import { IconZh } from './icons/zh'; import { IconEn } from './icons/en'; @@ -29,7 +29,7 @@ export default class ZhEn extends PureComponent<PluginProps> { globalLocale.setLocale(isZh ? 'en-US' : 'zh-CN'); }}> {isZh ? <IconZh size={20} /> : <IconEn size={20} />} - <EmbedTip direction="right">{intl('To Locale')}</EmbedTip> + <Tip direction="right">{intl('To Locale')}</Tip> </div> ); } diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index 1665889d8..8036d21c7 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -2,8 +2,8 @@ import LowCodeRenderer from '@ali/lowcode-react-renderer'; import { ReactInstance, Fragment, Component, createElement } from 'react'; import { observer } from '@recore/obx-react'; import { SimulatorRenderer } from './renderer'; -import './renderer.less'; import { host } from './host'; +import './renderer.less'; export default class SimulatorRendererView extends Component<{ renderer: SimulatorRenderer }> { render() { diff --git a/packages/react-simulator-renderer/src/renderer.ts b/packages/react-simulator-renderer/src/renderer.ts index 9cf819df0..1aa4ba534 100644 --- a/packages/react-simulator-renderer/src/renderer.ts +++ b/packages/react-simulator-renderer/src/renderer.ts @@ -3,15 +3,13 @@ import { render as reactRender } from 'react-dom'; import { host } from './host'; import SimulatorRendererView from './renderer-view'; import { computed, obx } from '@recore/obx'; -import { Asset, isReactComponent } from '@ali/lowcode-globals'; +import { Asset, isReactComponent } from '@ali/lowcode-utils'; import { getClientRects } from './utils/get-client-rects'; import loader from './utils/loader'; import { reactFindDOMNodes, FIBER_KEY } from './utils/react-find-dom-nodes'; -import { isESModule } from '@ali/lowcode-globals'; -import { isElement } from '@ali/lowcode-globals'; -import { cursor } from '@ali/lowcode-globals'; -import { setNativeSelection } from '@ali/lowcode-globals'; -import { RootSchema, NpmInfo } from '@ali/lowcode-globals'; +import { isESModule, isElement, cursor, setNativeSelection } from '@ali/lowcode-utils'; +import { RootSchema, NpmInfo } from '@ali/lowcode-types'; +// just use types import { BuiltinSimulatorRenderer, NodeInstance } from '@ali/lowcode-designer'; import Slot from './builtin-components/slot'; import Leaf from './builtin-components/leaf'; diff --git a/packages/react-simulator-renderer/src/utils/get-client-rects.ts b/packages/react-simulator-renderer/src/utils/get-client-rects.ts index 8de6b9dd9..f514c0a39 100644 --- a/packages/react-simulator-renderer/src/utils/get-client-rects.ts +++ b/packages/react-simulator-renderer/src/utils/get-client-rects.ts @@ -1,4 +1,4 @@ -import { isElement } from '@ali/lowcode-globals'; +import { isElement } from '@ali/lowcode-utils'; // a range for test TextNode clientRect const cycleRange = document.createRange(); diff --git a/packages/react-simulator-renderer/src/utils/loader.ts b/packages/react-simulator-renderer/src/utils/loader.ts index 8fe87912b..3bba55ba1 100644 --- a/packages/react-simulator-renderer/src/utils/loader.ts +++ b/packages/react-simulator-renderer/src/utils/loader.ts @@ -11,7 +11,7 @@ import { assetItem, AssetItem, isCSSUrl, -} from '@ali/lowcode-globals'; +} from '@ali/lowcode-utils'; function parseAssetList(scripts: any, styles: any, assets: AssetList, level?: AssetLevel) { for (const asset of assets) { diff --git a/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts b/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts index 38fdd378f..f7dde3925 100644 --- a/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts +++ b/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts @@ -1,5 +1,5 @@ import { ReactInstance } from 'react'; -import { isElement } from '@ali/lowcode-globals'; +import { isElement } from '@ali/lowcode-utils'; import { isDOMNode } from './is-dom-node'; export const FIBER_KEY = '_reactInternalFiber'; diff --git a/packages/setters/package.json b/packages/setters/package.json index 218fed58d..4f6441d3d 100644 --- a/packages/setters/package.json +++ b/packages/setters/package.json @@ -22,7 +22,7 @@ "@ali/iceluna-comp-react-node": "^1.0.5", "@ali/iceluna-sdk": "^1.0.5-beta.24", "@ali/lc-style-setter": "^0.0.1", - "@ali/lowcode-globals": "^0.9.3", + "@ali/lowcode-editor-core": "^0.9.3", "@alifd/next": "^1.19.16", "acorn": "^6.4.1", "classnames": "^2.2.6", diff --git a/packages/setters/src/index.tsx b/packages/setters/src/index.tsx index 21371311e..90ee52426 100644 --- a/packages/setters/src/index.tsx +++ b/packages/setters/src/index.tsx @@ -1,7 +1,6 @@ import { registerSetter, isJSSlot } from '@ali/lowcode-globals'; import { DatePicker, Input, Radio, Select, Switch, NumberPicker } from '@alifd/next'; import ExpressionSetter from './expression-setter'; -import MixinSetter from './mixin-setter'; import ColorSetter from './color-setter'; import JsonSetter from './json-setter'; import EventsSetter from './events-setter'; @@ -70,7 +69,6 @@ const builtinSetters: any = { }, recommend: true, }, - MixinSetter, RadioGroupSetter, TextAreaSetter, DateSetter, diff --git a/packages/setters/src/mixin-setter/index.tsx b/packages/setters/src/mixin-setter/index.tsx index 843c099af..a77664758 100644 --- a/packages/setters/src/mixin-setter/index.tsx +++ b/packages/setters/src/mixin-setter/index.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Dropdown, Button, Menu, Icon } from '@alifd/next'; -import { getSetter } from '@ali/lowcode-globals'; +import { getSetter } from '@ali/lowcode-editor-core'; import { generateI18n } from './locale/utils'; import zhCN from './locale/zh-CN'; diff --git a/packages/setters/src/style-setter/index.tsx b/packages/setters/src/style-setter/index.tsx index a32e25142..be7001d63 100644 --- a/packages/setters/src/style-setter/index.tsx +++ b/packages/setters/src/style-setter/index.tsx @@ -2,7 +2,7 @@ import './style.less'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import LowStyleSetter from '@ali/lc-style-setter'; -import { globalLocale } from '@ali/lowcode-globals'; +import { globalLocale } from '@ali/lowcode-editor-core'; export default class StyleSetter extends Component{ diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100644 index 000000000..ea73f17c2 --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1 @@ +类型集合 diff --git a/packages/types/build.json b/packages/types/build.json new file mode 100644 index 000000000..bd5cf18dd --- /dev/null +++ b/packages/types/build.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "build-plugin-component" + ] +} diff --git a/packages/globals/package.json b/packages/types/package.json similarity index 66% rename from packages/globals/package.json rename to packages/types/package.json index 99d853a7b..9bbd9d826 100644 --- a/packages/globals/package.json +++ b/packages/types/package.json @@ -1,36 +1,22 @@ { - "name": "@ali/lowcode-globals", - "version": "0.9.3", - "description": "Globals api for Ali lowCode engine", - "license": "MIT", + "name": "@ali/lowcode-types", + "version": "0.8.0", + "description": "Types for Ali lowCode engine", + "files": [ + "es", + "lib" + ], "main": "lib/index.js", "module": "es/index.js", - "files": [ - "lib", - "es" - ], "scripts": { "build": "build-scripts build --skip-demo", - "cloud-build": "build-scripts build --skip-demo --config cloud-build.json", "test": "ava", "test:snapshot": "ava --update-snapshots" }, - "ava": { - "compileEnhancements": false, - "extensions": [ - "ts" - ], - "require": [ - "ts-node/register" - ], - "snapshotDir": "test/fixtures/__snapshots__" - }, "dependencies": { "@alifd/next": "^1.19.16", - "@recore/obx": "^1.0.8", - "@recore/obx-react": "^1.0.7", "classnames": "^2.2.6", - "power-di": "^2.2.4", + "monaco-editor": "^0.20.0", "react": "^16", "react-dom": "^16.7.0" }, @@ -40,10 +26,20 @@ "@types/node": "^13.7.1", "@types/react": "^16", "@types/react-dom": "^16", - "build-plugin-component": "^0.2.11", + "build-plugin-component": "^0.2.10", "build-plugin-fusion": "^0.1.0", "build-plugin-moment-locales": "^0.1.0" }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + }, "publishConfig": { "registry": "https://registry.npm.alibaba-inc.com" } diff --git a/packages/globals/src/types/data-source.ts b/packages/types/src/data-source.ts similarity index 100% rename from packages/globals/src/types/data-source.ts rename to packages/types/src/data-source.ts diff --git a/packages/editor-core/src/definitions.ts b/packages/types/src/editor.ts similarity index 60% rename from packages/editor-core/src/definitions.ts rename to packages/types/src/editor.ts index 52fff0e7b..9932a174a 100644 --- a/packages/editor-core/src/definitions.ts +++ b/packages/types/src/editor.ts @@ -1,5 +1,38 @@ -import * as React from 'react'; -import Editor from './editor'; +import { EventEmitter } from 'events'; +import { ReactNode, ReactElement, RefObject, ComponentType } from 'react'; +import { NpmInfo } from './npm'; +import { RegisterOptions } from 'power-di'; + +export type KeyType = Function | symbol | string; +export type ClassType = Function | (new (...args: any[]) => any); +export interface GetOptions { + forceNew?: boolean; + sourceCls?: ClassType; +} +export type GetReturnType<T, ClsType> = T extends undefined + ? ClsType extends { + prototype: infer R; + } + ? R + : any + : T; + +export interface IEditor extends EventEmitter { + get<T = undefined, KeyOrType = any>(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType<T, KeyOrType> | undefined; + + has(keyOrType: KeyType): boolean; + + set(key: KeyType, data: any): void; + + onceGot<T = undefined, KeyOrType extends KeyType = any>(keyOrType: KeyOrType): Promise<GetReturnType<T, KeyOrType>>; + + onGot<T = undefined, KeyOrType extends KeyType = any>( + keyOrType: KeyOrType, + fn: (data: GetReturnType<T, KeyOrType>) => void, + ): () => void; + + register(data: any, key?: KeyType, options?: RegisterOptions): void; +} export interface EditorConfig { skeleton?: SkeletonConfig; @@ -13,17 +46,8 @@ export interface EditorConfig { i18n?: I18nConfig; } -export interface NpmConfig { - version: string; - package: string; - main?: string; - exportName?: string; - subName?: string; - destructuring?: boolean; -} - export interface SkeletonConfig { - config: NpmConfig; + config: NpmInfo; props?: object; handler?: (config: EditorConfig) => EditorConfig; } @@ -59,7 +83,7 @@ export interface PluginConfig { panelProps?: object; linkProps?: object; }; - config?: NpmConfig; + config?: NpmInfo; pluginProps?: object; } @@ -68,14 +92,14 @@ export type HooksConfig = HookConfig[]; export interface HookConfig { message: string; type: 'on' | 'once'; - handler: (editor: Editor, ...args: any[]) => void; + handler: (editor: IEditor, ...args: any[]) => void; } export type ShortCutsConfig = ShortCutConfig[]; export interface ShortCutConfig { keyboard: string; - handler: (editor: Editor, ev: Event, keymaster: any) => void; + handler: (editor: IEditor, ev: Event, keymaster: any) => void; } export type UtilsConfig = UtilConfig[]; @@ -83,14 +107,14 @@ export type UtilsConfig = UtilConfig[]; export interface UtilConfig { name: string; type: 'npm' | 'function'; - content: NpmConfig | ((...args: []) => any); + content: NpmInfo | ((...args: []) => any); } export type ConstantsConfig = object; export interface LifeCyclesConfig { - init?: (editor: Editor) => any; - destroy?: (editor: Editor) => any; + init?: (editor: IEditor) => any; + destroy?: (editor: IEditor) => any; } export type LocaleType = 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP'; @@ -113,19 +137,19 @@ export interface Utils { } export interface PluginProps { - editor: Editor; + editor: IEditor; config: PluginConfig; i18n?: I18nFunction; - ref?: React.RefObject<React.ReactElement>; + ref?: RefObject<ReactElement>; [key: string]: any; } -export type Plugin = React.ReactNode & { +export type Plugin = ReactNode & { open?: () => boolean | void | Promise<any>; close?: () => boolean | void | Promise<any>; }; -export type HOCPlugin = React.ReactNode & { +export type HOCPlugin = ReactNode & { open: () => Promise<any>; close: () => Promise<any>; }; @@ -134,8 +158,8 @@ export interface PluginSet { [key: string]: HOCPlugin; } -export type PluginClass = React.ComponentType<PluginProps> & { - init?: (editor: Editor) => void; +export type PluginClass = ComponentType<PluginProps> & { + init?: (editor: IEditor) => void; defaultProps?: { locale?: LocaleType; messages?: I18nMessages; diff --git a/packages/globals/src/types/field-config.ts b/packages/types/src/field-config.ts similarity index 99% rename from packages/globals/src/types/field-config.ts rename to packages/types/src/field-config.ts index 17a4cfd78..7e0ff6b38 100644 --- a/packages/globals/src/types/field-config.ts +++ b/packages/types/src/field-config.ts @@ -2,7 +2,6 @@ import { TitleContent } from './title'; import { SetterType, DynamicSetter } from './setter-config'; import { SettingTarget } from './setting-target'; - export interface FieldExtraProps { /** * 是否必填参数 diff --git a/packages/globals/src/types/i18n.ts b/packages/types/src/i18n.ts similarity index 100% rename from packages/globals/src/types/i18n.ts rename to packages/types/src/i18n.ts diff --git a/packages/globals/src/types/icon.ts b/packages/types/src/icon.ts similarity index 100% rename from packages/globals/src/types/icon.ts rename to packages/types/src/icon.ts diff --git a/packages/globals/src/types/index.ts b/packages/types/src/index.ts similarity index 93% rename from packages/globals/src/types/index.ts rename to packages/types/src/index.ts index 9ebb731ff..31aa669f3 100644 --- a/packages/globals/src/types/index.ts +++ b/packages/types/src/index.ts @@ -1,4 +1,5 @@ export * from './data-source'; +export * from './editor'; export * from './field-config'; export * from './i18n'; export * from './icon'; diff --git a/packages/globals/src/types/metadata.ts b/packages/types/src/metadata.ts similarity index 100% rename from packages/globals/src/types/metadata.ts rename to packages/types/src/metadata.ts diff --git a/packages/globals/src/types/npm.ts b/packages/types/src/npm.ts similarity index 100% rename from packages/globals/src/types/npm.ts rename to packages/types/src/npm.ts diff --git a/packages/globals/src/types/prop-config.ts b/packages/types/src/prop-config.ts similarity index 100% rename from packages/globals/src/types/prop-config.ts rename to packages/types/src/prop-config.ts diff --git a/packages/globals/src/types/schema.ts b/packages/types/src/schema.ts similarity index 100% rename from packages/globals/src/types/schema.ts rename to packages/types/src/schema.ts diff --git a/packages/globals/src/types/setter-config.ts b/packages/types/src/setter-config.ts similarity index 96% rename from packages/globals/src/types/setter-config.ts rename to packages/types/src/setter-config.ts index add5b59ca..7bac84279 100644 --- a/packages/globals/src/types/setter-config.ts +++ b/packages/types/src/setter-config.ts @@ -1,4 +1,4 @@ -import { isReactComponent } from '../utils'; +import { isReactComponent } from '@ali/lowcode-utils'; import { ComponentType, ReactElement, isValidElement } from 'react'; import { TitleContent } from './title'; import { SettingTarget } from './setting-target'; @@ -31,7 +31,6 @@ export interface SetterConfig { */ export type SetterType = SetterConfig | SetterConfig[] | string | CustomView; - export function isSetterConfig(obj: any): obj is SetterConfig { return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj); } diff --git a/packages/globals/src/types/setting-target.ts b/packages/types/src/setting-target.ts similarity index 96% rename from packages/globals/src/types/setting-target.ts rename to packages/types/src/setting-target.ts index 94a589561..3848fc154 100644 --- a/packages/globals/src/types/setting-target.ts +++ b/packages/types/src/setting-target.ts @@ -1,4 +1,4 @@ -import { IEditor } from '../di'; +import { IEditor } from './editor'; export interface SettingTarget { /** diff --git a/packages/globals/src/types/tip.ts b/packages/types/src/tip.ts similarity index 100% rename from packages/globals/src/types/tip.ts rename to packages/types/src/tip.ts diff --git a/packages/globals/src/types/title.ts b/packages/types/src/title.ts similarity index 99% rename from packages/globals/src/types/title.ts rename to packages/types/src/title.ts index e9a089b17..3be21d84e 100644 --- a/packages/globals/src/types/title.ts +++ b/packages/types/src/title.ts @@ -3,7 +3,6 @@ import { I18nData } from './i18n'; import { TipContent } from './tip'; import { IconType } from './icon'; - export interface TitleConfig { label?: I18nData | ReactNode; tip?: TipContent; diff --git a/packages/globals/src/types/utils.ts b/packages/types/src/utils.ts similarity index 100% rename from packages/globals/src/types/utils.ts rename to packages/types/src/utils.ts diff --git a/packages/globals/src/types/value-type.ts b/packages/types/src/value-type.ts similarity index 100% rename from packages/globals/src/types/value-type.ts rename to packages/types/src/value-type.ts diff --git a/packages/globals/tsconfig.json b/packages/types/tsconfig.json similarity index 73% rename from packages/globals/tsconfig.json rename to packages/types/tsconfig.json index 91c180bdd..c37b76ecc 100644 --- a/packages/globals/tsconfig.json +++ b/packages/types/tsconfig.json @@ -3,5 +3,7 @@ "compilerOptions": { "outDir": "lib" }, - "include": ["./src/"], + "include": [ + "./src/" + ] } diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..e69de29bb diff --git a/packages/globals/src/utils/asset.ts b/packages/utils/src/asset.ts similarity index 100% rename from packages/globals/src/utils/asset.ts rename to packages/utils/src/asset.ts diff --git a/packages/globals/src/utils/clone-deep.ts b/packages/utils/src/clone-deep.ts similarity index 100% rename from packages/globals/src/utils/clone-deep.ts rename to packages/utils/src/clone-deep.ts diff --git a/packages/globals/src/utils/create-content.ts b/packages/utils/src/create-content.ts similarity index 100% rename from packages/globals/src/utils/create-content.ts rename to packages/utils/src/create-content.ts diff --git a/packages/globals/src/utils/create-icon.tsx b/packages/utils/src/create-icon.tsx similarity index 93% rename from packages/globals/src/utils/create-icon.tsx rename to packages/utils/src/create-icon.tsx index 0f9ec82c7..9a3a7c74d 100644 --- a/packages/globals/src/utils/create-icon.tsx +++ b/packages/utils/src/create-icon.tsx @@ -1,7 +1,7 @@ -import { Icon } from '@alifd/next'; import { isValidElement, ReactNode, createElement, cloneElement } from 'react'; +import { Icon } from '@alifd/next'; +import { IconType } from '@ali/lowcode-types'; import { isReactComponent } from './is-react'; -import { IconType } from '../types'; const URL_RE = /^(https?:)\/\//i; diff --git a/packages/globals/src/utils/cursor.less b/packages/utils/src/cursor.less similarity index 100% rename from packages/globals/src/utils/cursor.less rename to packages/utils/src/cursor.less diff --git a/packages/globals/src/utils/cursor.ts b/packages/utils/src/cursor.ts similarity index 100% rename from packages/globals/src/utils/cursor.ts rename to packages/utils/src/cursor.ts diff --git a/packages/globals/src/utils/get-prototype-of.ts b/packages/utils/src/get-prototype-of.ts similarity index 100% rename from packages/globals/src/utils/get-prototype-of.ts rename to packages/utils/src/get-prototype-of.ts diff --git a/packages/globals/src/utils/has-own-property.ts b/packages/utils/src/has-own-property.ts similarity index 100% rename from packages/globals/src/utils/has-own-property.ts rename to packages/utils/src/has-own-property.ts diff --git a/packages/globals/src/utils/index.ts b/packages/utils/src/index.ts similarity index 86% rename from packages/globals/src/utils/index.ts rename to packages/utils/src/index.ts index c61e07cd1..3df01d928 100644 --- a/packages/globals/src/utils/index.ts +++ b/packages/utils/src/index.ts @@ -1,22 +1,20 @@ -export * from './create-icon'; -export * from './is-react'; -export * from './unique-id'; -export * from './create-content'; export * from './asset'; export * from './clone-deep'; +export * from './create-content'; +export * from './create-icon'; export * from './cursor'; export * from './get-prototype-of'; export * from './has-own-property'; export * from './is-css-url'; export * from './is-element'; export * from './is-es-module'; +export * from './is-form-event'; export * from './is-function'; export * from './is-object'; export * from './is-plain-object'; +export * from './is-react'; export * from './navtive-selection'; export * from './set-prototype-of'; export * from './shallow-equal'; +export * from './svg-icon'; export * from './unique-id'; -export * from './get-public-path'; -export * from './is-form-event'; -export * from './hotkey'; diff --git a/packages/globals/src/utils/is-css-url.ts b/packages/utils/src/is-css-url.ts similarity index 100% rename from packages/globals/src/utils/is-css-url.ts rename to packages/utils/src/is-css-url.ts diff --git a/packages/globals/src/utils/is-element.ts b/packages/utils/src/is-element.ts similarity index 100% rename from packages/globals/src/utils/is-element.ts rename to packages/utils/src/is-element.ts diff --git a/packages/globals/src/utils/is-es-module.ts b/packages/utils/src/is-es-module.ts similarity index 100% rename from packages/globals/src/utils/is-es-module.ts rename to packages/utils/src/is-es-module.ts diff --git a/packages/globals/src/utils/is-form-event.ts b/packages/utils/src/is-form-event.ts similarity index 100% rename from packages/globals/src/utils/is-form-event.ts rename to packages/utils/src/is-form-event.ts diff --git a/packages/globals/src/utils/is-function.ts b/packages/utils/src/is-function.ts similarity index 100% rename from packages/globals/src/utils/is-function.ts rename to packages/utils/src/is-function.ts diff --git a/packages/globals/src/utils/is-object.ts b/packages/utils/src/is-object.ts similarity index 100% rename from packages/globals/src/utils/is-object.ts rename to packages/utils/src/is-object.ts diff --git a/packages/globals/src/utils/is-plain-object.ts b/packages/utils/src/is-plain-object.ts similarity index 100% rename from packages/globals/src/utils/is-plain-object.ts rename to packages/utils/src/is-plain-object.ts diff --git a/packages/globals/src/utils/is-react.ts b/packages/utils/src/is-react.ts similarity index 100% rename from packages/globals/src/utils/is-react.ts rename to packages/utils/src/is-react.ts diff --git a/packages/globals/src/utils/navtive-selection.ts b/packages/utils/src/navtive-selection.ts similarity index 100% rename from packages/globals/src/utils/navtive-selection.ts rename to packages/utils/src/navtive-selection.ts diff --git a/packages/globals/src/utils/set-prototype-of.ts b/packages/utils/src/set-prototype-of.ts similarity index 100% rename from packages/globals/src/utils/set-prototype-of.ts rename to packages/utils/src/set-prototype-of.ts diff --git a/packages/globals/src/utils/shallow-equal.ts b/packages/utils/src/shallow-equal.ts similarity index 100% rename from packages/globals/src/utils/shallow-equal.ts rename to packages/utils/src/shallow-equal.ts diff --git a/packages/globals/src/components/svg-icon.tsx b/packages/utils/src/svg-icon.tsx similarity index 100% rename from packages/globals/src/components/svg-icon.tsx rename to packages/utils/src/svg-icon.tsx diff --git a/packages/globals/src/utils/unique-id.ts b/packages/utils/src/unique-id.ts similarity index 100% rename from packages/globals/src/utils/unique-id.ts rename to packages/utils/src/unique-id.ts diff --git a/packages/vision-polyfill/src/skeleton/area.ts b/packages/vision-polyfill/src/skeleton/area.ts index 9c35e1289..3877e175d 100644 --- a/packages/vision-polyfill/src/skeleton/area.ts +++ b/packages/vision-polyfill/src/skeleton/area.ts @@ -1,7 +1,7 @@ import { obx, computed } from '@ali/lowcode-globals'; -import WidgetContainer from './widget-container'; +import WidgetContainer from './widget/widget-container'; import { Skeleton } from './skeleton'; -import { IWidget } from './widget'; +import { IWidget } from './widget/widget'; import { IWidgetBaseConfig } from './types'; export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget = IWidget> { diff --git a/packages/vision-polyfill/src/skeleton/components/array-setter/bugs.md b/packages/vision-polyfill/src/skeleton/components/array-setter/bugs.md new file mode 100644 index 000000000..8fe96cc16 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/array-setter/bugs.md @@ -0,0 +1,5 @@ +* 拖拽排序有问题 +* forceInline 有问题 +* 部分改变不响应 +* 样式还原 +* autofocus diff --git a/packages/vision-polyfill/src/skeleton/components/array-setter/index.tsx b/packages/vision-polyfill/src/skeleton/components/array-setter/index.tsx new file mode 100644 index 000000000..344bd8a0f --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/array-setter/index.tsx @@ -0,0 +1,279 @@ +import { Component, Fragment } from 'react'; +import { Icon, Button, Message } from '@alifd/next'; +import { Title, SetterType, FieldConfig, SetterConfig } from '@ali/lowcode-globals'; +import { createSettingFieldView } from '../../settings/settings-pane'; +import { PopupContext, PopupPipe } from '../../popup'; +import Sortable from './sortable'; +import './style.less'; +import { SettingField } from '@ali/lowcode-designer'; + +interface ArraySetterState { + items: SettingField[]; + itemsMap: Map<string | number, SettingField>; + prevLength: number; +} + +interface ArraySetterProps { + value: any[]; + field: SettingField; + itemSetter?: SetterType; + columns?: FieldConfig[]; + multiValue?: boolean; +} + +export class ListSetter extends Component<ArraySetterProps, ArraySetterState> { + static getDerivedStateFromProps(props: ArraySetterProps, state: ArraySetterState) { + const { value, field } = props; + const newLength = value && Array.isArray(value) ? value.length : 0; + if (state && state.prevLength === newLength) { + return null; + } + + // props value length change will go here + const originLength = state ? state.items.length : 0; + if (state && originLength === newLength) { + return { + prevLength: newLength, + }; + } + + const itemsMap = state ? state.itemsMap : new Map<string | number, SettingField>(); + let items = state ? state.items.slice() : []; + if (newLength > originLength) { + for (let i = originLength; i < newLength; i++) { + const item = field.createField({ + name: i, + setter: props.itemSetter, + // FIXME: + forceInline: 1, + }); + items[i] = item; + itemsMap.set(item.id, item); + } + } else if (newLength < originLength) { + const deletes = items.splice(newLength); + deletes.forEach((item) => { + itemsMap.delete(item.id); + }); + } + return { + items, + itemsMap, + prevLength: newLength, + }; + } + + state: ArraySetterState = { + items: [], + itemsMap: new Map<string | number, SettingField>(), + prevLength: 0, + }; + + onSort(sortedIds: Array<string | number>) { + const { itemsMap } = this.state; + const items = sortedIds.map((id, index) => { + const item = itemsMap.get(id)!; + item.setKey(index); + return item; + }); + this.setState({ + items, + }); + } + + private scrollToLast: boolean = false; + onAdd() { + const { items, itemsMap } = this.state; + const { itemSetter } = this.props; + const initialValue = typeof itemSetter === 'object' ? (itemSetter as any).initialValue : null; + const item = this.props.field.createField({ + name: items.length, + setter: itemSetter, + // FIXME: + forceInline: 1, + }); + items.push(item); + itemsMap.set(item.id, item); + item.setValue(typeof initialValue === 'function' ? initialValue(item) : initialValue); + this.scrollToLast = true; + this.setState({ + items: items.slice(), + }); + } + + onRemove(field: SettingField) { + const { items } = this.state; + let i = items.indexOf(field); + if (i < 0) { + return; + } + items.splice(i, 1); + const l = items.length; + while (i < l) { + items[i].setKey(i); + i++; + } + field.remove(); + this.setState({ items: items.slice() }); + } + + componentWillUnmount() { + this.state.items.forEach((field) => { + field.purge(); + }); + } + + shouldComponentUpdate(_: any, nextState: ArraySetterState) { + if (nextState.items !== this.state.items) { + return true; + } + return false; + } + + render() { + let columns: any = null; + if (this.props.columns) { + columns = this.props.columns.map((column) => <Title key={column.name} title={column.title || (column.name as string)} />); + } + + const { items } = this.state; + const scrollToLast = this.scrollToLast; + this.scrollToLast = false; + const lastIndex = items.length - 1; + + const content = + items.length > 0 ? ( + <div className="lc-setter-list-scroll-body"> + <Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}> + {items.map((field, index) => ( + <ArrayItem + key={field.id} + scrollIntoView={scrollToLast && index === lastIndex} + field={field} + onRemove={this.onRemove.bind(this, field)} + /> + ))} + </Sortable> + </div> + ) : this.props.multiValue ? ( + <Message type="warning">当前选择了多个节点,且值不一致,修改会覆盖所有值</Message> + ) : ( + <Message type="notice">当前项目为空</Message> + ); + + return ( + <div className="lc-setter-list lc-block-setter"> + {/*<div className="lc-block-setter-actions"> + <Button size="medium" onClick={this.onAdd.bind(this)}> + <Icon type="add" /> + <span>添加</span> + </Button> + </div>*/} + {columns && <div className="lc-setter-list-columns">{columns}</div>} + {content} + <Button className="lc-setter-list-add" type="primary" onClick={this.onAdd.bind(this)}> + <Icon type="add" /> + <span>添加一项</span> + </Button> + </div> + ); + } +} + +class ArrayItem extends Component<{ + field: SettingField; + onRemove: () => void; + scrollIntoView: boolean; +}> { + shouldComponentUpdate() { + return false; + } + private shell?: HTMLDivElement | null; + componentDidMount() { + if (this.props.scrollIntoView && this.shell) { + this.shell.parentElement!.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } + render() { + const { onRemove, field } = this.props; + return ( + <div className="lc-listitem" ref={(ref) => (this.shell = ref)}> + <div draggable className="lc-listitem-handler"> + <Icon type="ellipsis" size="small" /> + </div> + <div className="lc-listitem-body">{createSettingFieldView(field, field.parent)}</div> + <div className="lc-listitem-actions"> + <div className="lc-listitem-action" onClick={onRemove}> + <Icon type="ashbin" size="small" /> + </div> + </div> + </div> + ); + } +} + +class TableSetter extends ListSetter { + // todo: + // forceInline = 1 + // has more actions +} + +export default class ArraySetter extends Component<{ + value: any[]; + field: SettingField; + itemSetter?: SetterType; + mode?: 'popup' | 'list'; + forceInline?: boolean; + multiValue?: boolean; +}> { + static contextType = PopupContext; + private pipe: any; + render() { + const { mode, forceInline, ...props } = this.props; + const { field, itemSetter } = props; + let columns: FieldConfig[] | undefined; + if ((itemSetter as SetterConfig)?.componentName === 'ObjectSetter') { + const items: FieldConfig[] = (itemSetter as any).props?.config?.items; + if (items && Array.isArray(items)) { + columns = items.filter((item) => item.isRequired || item.important || (item.setter as any)?.isRequired); + if (columns.length > 4) { + columns = columns.slice(0, 4); + } + } + } + + if (mode === 'popup' || forceInline) { + const title = ( + <Fragment> + 编辑: + <Title title={field.title} /> + </Fragment> + ); + if (!this.pipe) { + let width = 360; + if (columns) { + if (columns.length === 3) { + width = 480; + } else if (columns.length > 3) { + width = 600; + } + } + this.pipe = (this.context as PopupPipe).create({ width }); + } + this.pipe.send(<TableSetter key={field.id} {...props} columns={columns} />, title); + return ( + <Button + type={forceInline ? 'normal' : 'primary'} + onClick={(e) => { + this.pipe.show((e as any).target, field.id); + }} + > + <Icon type="edit" /> + {forceInline ? title : '编辑数组'} + </Button> + ); + } else { + return <ListSetter {...props} columns={columns?.slice(0, 2)} />; + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/array-setter/sortable.less b/packages/vision-polyfill/src/skeleton/components/array-setter/sortable.less new file mode 100644 index 000000000..12e9512cd --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/array-setter/sortable.less @@ -0,0 +1,29 @@ +.lc-sortable { + position: relative; + + .lc-sortable-card { + box-sizing: border-box; + &:after, &:before { + content: ""; + display: table; + } + &:after { + clear: both; + } + + &.lc-dragging { + outline: 2px dashed var(--color-brand); + outline-offset: -2px; + > * { + visibility: hidden; + } + border-color: transparent !important; + box-shadow: none !important; + background: transparent !important; + } + } + + [draggable] { + cursor: ns-resize; + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/array-setter/sortable.tsx b/packages/vision-polyfill/src/skeleton/components/array-setter/sortable.tsx new file mode 100644 index 000000000..3329470f1 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/array-setter/sortable.tsx @@ -0,0 +1,220 @@ +import { Component, Children, ReactElement } from 'react'; +import classNames from 'classnames'; +import './sortable.less'; + +class Sortable extends Component<{ + className?: string; + itemClassName?: string; + onSort?: (sortedIds: Array<string | number>) => void; + dragImageSourceHandler?: (elem: Element) => Element; + children: ReactElement[]; +}> { + private shell?: HTMLDivElement | null; + private items?: Array<string | number>; + private willDetach?: () => void; + componentDidMount() { + const box = this.shell!; + + let isDragEnd: boolean = false; + + /** + * target node to be dragged + */ + let source: Element | null; + + /** + * node to be placed + */ + let ref: Element | null; + + /** + * next sibling of the source node + */ + let origRef: Element | null; + + /** + * accurately locate the node from event + */ + const locate = (e: DragEvent) => { + let y = e.clientY; + if (e.view !== window && e.view!.frameElement) { + y += e.view!.frameElement.getBoundingClientRect().top; + } + let node = box.firstElementChild as HTMLDivElement; + while (node) { + if (node !== source && node.dataset.id) { + const rect = node.getBoundingClientRect(); + + if (rect.height <= 0) continue; + if (y < rect.top + rect.height / 2) { + break; + } + } + node = node.nextElementSibling as HTMLDivElement; + } + return node; + }; + + /** + * find the source node + */ + const getSource = (e: DragEvent) => { + const target = e.target as Element; + if (!target || !box.contains(target) || target === box) { + return null; + } + + let node = box.firstElementChild; + while (node) { + if (node.contains(target)) { + return node; + } + node = node.nextElementSibling; + } + + return null; + }; + + const sort = (beforeId: string | number | null | undefined) => { + if (!source) return; + + const sourceId = (source as HTMLDivElement).dataset.id; + const items = this.items!; + const origIndex = items.findIndex(id => id == sourceId); + + let newIndex = beforeId ? items.findIndex(id => id == beforeId) : items.length; + + if (origIndex < 0 || newIndex < 0) return; + if (this.props.onSort) { + if (newIndex > origIndex) { + newIndex -= 1; + } + if (origIndex === newIndex) return; + const item = items.splice(origIndex, 1); + items.splice(newIndex, 0, item[0]); + + this.props.onSort(items); + } + }; + + const dragstart = (e: DragEvent) => { + isDragEnd = false; + source = getSource(e); + if (!source) { + return false; + } + origRef = source.nextElementSibling; + const rect = source.getBoundingClientRect(); + let dragSource = source; + if (this.props.dragImageSourceHandler) { + dragSource = this.props.dragImageSourceHandler(source); + } + if (e.dataTransfer) { + e.dataTransfer.setDragImage(dragSource, e.clientX - rect.left, e.clientY - rect.top); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.dropEffect = 'move'; + try { + e.dataTransfer.setData('application/json', {} as any); + } catch (ex) { + // eslint-disable-line + } + } + + setTimeout(() => { + source!.classList.add('lc-dragging'); + }, 0); + return true; + }; + + const placeAt = (beforeRef: Element | null) => { + if (beforeRef) { + if (beforeRef !== source) { + box.insertBefore(source!, beforeRef); + } + } else { + box.appendChild(source!); + } + }; + + const adjust = (e: DragEvent) => { + if (isDragEnd) return; + ref = locate(e); + placeAt(ref); + }; + + let lastDragEvent: DragEvent | null; + const drag = (e: DragEvent) => { + if (!source) return; + e.preventDefault(); + if (lastDragEvent) { + if (lastDragEvent.clientX === e.clientX && lastDragEvent.clientY === e.clientY) { + return; + } + } + lastDragEvent = e; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + } + adjust(e); + }; + + const dragend = (e: DragEvent) => { + isDragEnd = true; + if (!source) return; + e.preventDefault(); + source.classList.remove('lc-dragging'); + placeAt(origRef); + sort(ref ? (ref as HTMLDivElement).dataset.id : null); + source = null; + ref = null; + origRef = null; + lastDragEvent = null; + }; + + box.addEventListener('dragstart', dragstart); + document.addEventListener('dragover', drag); + document.addEventListener('drag', drag); + document.addEventListener('dragend', dragend); + + this.willDetach = () => { + box.removeEventListener('dragstart', dragstart); + document.removeEventListener('dragover', drag); + document.removeEventListener('drag', drag); + document.removeEventListener('dragend', dragend); + }; + } + + componentWillUnmount() { + if (this.willDetach) { + this.willDetach(); + } + } + + render() { + const { className, itemClassName, children } = this.props; + const items: Array<string | number> = []; + const cards = Children.map(children, child => { + const id = child.key!; + items.push(id); + return ( + <div key={id} data-id={id} className={classNames('lc-sortable-card', itemClassName)}> + {child} + </div> + ); + }); + this.items = items; + + return ( + <div + className={classNames('lc-sortable', className)} + ref={ref => { + this.shell = ref; + }} + > + {cards} + </div> + ); + } +} + +export default Sortable; diff --git a/packages/vision-polyfill/src/skeleton/components/array-setter/style.less b/packages/vision-polyfill/src/skeleton/components/array-setter/style.less new file mode 100644 index 000000000..8e0da024b --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/array-setter/style.less @@ -0,0 +1,103 @@ +.lc-setter-list { + [draggable] { + cursor: move; + } + color: var(--color-text); + + .next-btn { + display: inline-flex; + align-items: center; + line-height: 1 !important; + max-width: 100%; + text-overflow: ellipsis; + } + + .lc-setter-list-add { + display: block; + width: 100%; + margin-top: 8px;; + } + + + .lc-setter-list-columns { + display: flex; + > .lc-title { + flex: 1; + justify-content: center; + } + margin-left: 47px; + margin-right: 28px; + margin-bottom: 5px; + } + + .lc-setter-list-scroll-body { + margin: -8px -5px; + padding: 8px 10px; + overflow-y: auto; + max-height: 300px; + } + + .lc-setter-list-card { + border: 1px solid rgba(31,56,88,.2); + background-color: var(--color-block-background-light); + border-radius: 3px; + &:not(:last-child) { + margin-bottom: 5px; + } + + .lc-listitem { + position: relative; + outline: none; + display: flex; + align-items: stretch; + height: 34px; + + .lc-listitem-actions { + margin: 0 3px; + display: inline-flex; + align-items: center; + justify-content: flex-end; + .lc-listitem-action { + text-align: center; + cursor: pointer; + opacity: 0.6; + &:hover { + opacity: 1; + } + } + } + .lc-listitem-body { + flex: 1; + display: flex; + align-items: stretch; + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + .lc-field { + padding: 0 !important; + display: flex; + align-items: center; + >.lc-field-body { + justify-content: center; + } + } + > * { + width: 100%; + } + .next-btn { + display: block; + width: 100%; + } + } + .lc-listitem-handler { + margin-left: 2px; + display: inline-flex; + align-items: center; + .next-icon-ellipsis { + transform: rotate(90deg); + } + opacity: 0.6; + } + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/field/fields.tsx b/packages/vision-polyfill/src/skeleton/components/field/fields.tsx new file mode 100644 index 000000000..d36928604 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/field/fields.tsx @@ -0,0 +1,184 @@ +import { Component } from 'react'; +import classNames from 'classnames'; +import { Icon } from '@alifd/next'; +import { Title, TitleContent } from '@ali/lowcode-globals'; +import { PopupPipe, PopupContext } from '../popup'; +import './index.less'; + +export interface FieldProps { + className?: string; + title?: TitleContent | null; + defaultDisplay?: 'accordion' | 'inline' | 'block'; + collapsed?: boolean; + onExpandChange?: (expandState: boolean) => void; +} + +export class Field extends Component<FieldProps> { + state = { + collapsed: this.props.collapsed, + display: this.props.defaultDisplay || 'inline', + }; + + private toggleExpand = () => { + const { onExpandChange } = this.props; + const collapsed = !this.state.collapsed; + this.setState({ + collapsed, + }); + onExpandChange && onExpandChange(!collapsed); + }; + private body: HTMLDivElement | null = null; + private dispose?: () => void; + private deployBlockTesting() { + if (this.dispose) { + this.dispose(); + } + const body = this.body; + if (!body) { + return; + } + const check = () => { + const setter = body.firstElementChild; + if (setter && setter.classList.contains('lc-block-setter')) { + this.setState({ + display: 'block', + }); + } else { + this.setState({ + display: 'inline', + }); + } + }; + const observer = new MutationObserver(check); + check(); + observer.observe(body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'], + }); + this.dispose = () => observer.disconnect(); + } + componentDidMount() { + const { defaultDisplay } = this.props; + if (!defaultDisplay || defaultDisplay === 'inline') { + this.deployBlockTesting(); + } + } + componentWillUnmount() { + if (this.dispose) { + this.dispose(); + } + } + + render() { + const { className, children, title } = this.props; + const { display, collapsed } = this.state; + const isAccordion = display === 'accordion'; + return ( + <div + className={classNames(`lc-field lc-${display}-field`, className, { + 'lc-field-is-collapsed': isAccordion && collapsed, + })} + > + <div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}> + <div className="lc-field-title"> + <Title title={title || ''} /> + </div> + {isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />} + </div> + <div key="body" ref={(shell) => (this.body = shell)} className="lc-field-body"> + {children} + </div> + </div> + ); + } +} + +export interface PopupFieldProps extends FieldProps { + width?: number; +} + +export class PopupField extends Component<PopupFieldProps> { + static contextType = PopupContext; + private pipe: any; + + static defaultProps: PopupFieldProps = { + width: 300, + }; + + render() { + const { className, children, title, width } = this.props; + if (!this.pipe) { + this.pipe = (this.context as PopupPipe).create({ width }); + } + + const titleElement = title && ( + <div className="lc-field-title"> + <Title title={title} /> + </div> + ); + + this.pipe.send(<div className="lc-field-body">{children}</div>, titleElement); + + return ( + <div className={classNames('lc-field lc-popup-field', className)}> + {title && ( + <div + className="lc-field-head" + onClick={(e) => { + this.pipe.show((e as any).target); + }} + > + <div className="lc-field-title"> + <Title title={title} /> + </div> + <Icon className="lc-field-icon" type="arrow-left" size="xs" /> + </div> + )} + </div> + ); + } +} + +export interface EntryFieldProps extends FieldProps { + stageName?: string; +} + +export class EntryField extends Component<EntryFieldProps> { + render() { + const { stageName, title, className } = this.props; + const classNameList = classNames('engine-setting-field', 'engine-entry-field', className); + const fieldProps: any = {}; + + if (stageName) { + // 为 stage 切换奠定基础 + fieldProps['data-stage-target'] = 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 PlainField extends Component<FieldProps> { + render() { + const { className, children } = this.props; + return ( + <div className={classNames(`lc-field lc-plain-field`, className)}> + <div className="lc-field-body">{children}</div> + </div> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/field/index.less b/packages/vision-polyfill/src/skeleton/components/field/index.less new file mode 100644 index 000000000..5fecb74d9 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/field/index.less @@ -0,0 +1,154 @@ +@import '../variables.less'; + +@x-gap: 10px; +@y-gap: 8px; + +.lc-field { + // head + .lc-field-head { + display: flex; + align-items: center; + justify-content: space-between; + + .lc-field-title { + display: flex; + align-items: center; + } + .lc-field-icon { + margin-right: @x-gap; + transform-origin: center; + transition: transform 0.1s; + } + } + + &.lc-plain-field { + // for top-level style + padding: 8px 10px; + > .lc-field-body { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + } + } + + &.lc-inline-field { + display: flex; + align-items: center; + // for top-level style + padding: 8px 10px; + + > .lc-field-head { + width: 70px; + margin-right: 1px; + .lc-title-label { + width: 70px; + word-break: break-all; + } + } + > .lc-field-body { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + } + } + + &.lc-block-field, &.lc-accordion-field { + display: block; + &:not(:first-child) { + border-top: 1px solid var(--color-line-normal, @dark-alpha-2); + } + > .lc-field-head { + padding-left: @x-gap; + height: 32px; + display: flex; + align-items: center; + font-weight: 500; + background: var(--color-block-background-shallow, rgba(31,56,88,.06)); + border-bottom: 1px solid var(--color-line-normal, @dark-alpha-2); + color: var(--color-title, @white-alpha-2); + user-select: none; + } + + > .lc-field-body { + padding: @y-gap @x-gap/2; + } + + + .lc-inline-field { + border-top: 1px solid var(--color-line-normal, @dark-alpha-2); + } + } + + .lc-setter-actions { + display: flex; + align-items: center; + } + + &.lc-block-field { + position: relative; + >.lc-field-body>.lc-block-setter>.lc-setter-actions { + position: absolute; + right: 10px; + top: 0; + height: 32px; + display: flex; + align-items: center; + } + } + + &.lc-accordion-field { + // collapsed + &.lc-field-is-collapsed { + > .lc-field-head .lc-field-icon { + transform: rotate(180deg); + } + > .lc-field-body { + display: none; + } + } + + // 邻近的保持上下距离 + + .lc-field { + margin-top: @y-gap; + } + } + + // 2rd level reset + .lc-field-body { + .lc-inline-field { + padding: @y-gap @x-gap/2 0 @x-gap/2; + &:first-child { + padding-top: 0; + } + + .lc-accordion-field, +.lc-block-field { + margin-top: @y-gap; + } + } + + .lc-field { + border-top: none !important; + } + + .lc-accordion-field, .lc-block-field { + > .lc-field-head { + padding-left: @x-gap/2; + background: var(--color-block-background-light); + border-bottom-color: var(--color-line-light); + > .lc-field-icon { + margin-right: @x-gap/2; + } + } + } + + // 3rd level field title width should short + .lc-field-body .lc-inline-field { + > .lc-field-head { + width: 50px; + .lc-title-label { + width: 50px; + } + } + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/field/index.ts b/packages/vision-polyfill/src/skeleton/components/field/index.ts new file mode 100644 index 000000000..2f50d53ff --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/field/index.ts @@ -0,0 +1,28 @@ +import { ReactNode, createElement } from 'react'; +import { TitleContent } from '@ali/lowcode-globals'; +import './index.less'; +import { Field, PopupField, EntryField, PlainField } from './fields'; + +export interface FieldProps { + className?: string; + title?: TitleContent | null; + display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; + collapsed?: boolean; + onExpandChange?: (collapsed: boolean) => void; + [extra: string]: any; +} + +export function createField(props: FieldProps, children: ReactNode, type?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry') { + if (type === 'popup') { + return createElement(PopupField, props, children); + } + if (type === 'entry') { + return createElement(EntryField, props, children); + } + if (type === 'plain' || !props.title) { + return createElement(PlainField, props, children); + } + return createElement(Field, { ...props, defaultDisplay: type }, children); +} + +export { Field, PopupField, EntryField, PlainField }; diff --git a/packages/vision-polyfill/src/skeleton/components/mixed-setter/index.tsx b/packages/vision-polyfill/src/skeleton/components/mixed-setter/index.tsx new file mode 100644 index 000000000..5217322c2 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/mixed-setter/index.tsx @@ -0,0 +1,257 @@ +import React, { Component, isValidElement } from 'react'; +import classNames from 'classnames'; +import { Dropdown, Button, Menu } from '@alifd/next'; +import { + getSetter, + getSettersMap, + SetterConfig, + computed, + obx, + CustomView, + DynamicProps, + DynamicSetter, + TitleContent, + isSetterConfig, + Title, + createSetterContent, + observer, + isDynamicSetter, + shallowIntl, + EmbedTip, + isI18nData, +} from '@ali/lowcode-globals'; +import { IconConvert } from '../../icons/convert'; + +import './style.less'; +import { SettingField } from '@ali/lowcode-designer'; + +export interface SetterItem { + name: string; + title: TitleContent; + setter: string | DynamicSetter | CustomView; + props?: object | DynamicProps; + condition?: (field: SettingField) => boolean; + initialValue?: any | ((field: SettingField) => any); + list: boolean; +} + +function nomalizeSetters(setters?: Array<string | SetterConfig | CustomView | DynamicSetter>): SetterItem[] { + if (!setters) { + const normalized: SetterItem[] = []; + getSettersMap().forEach((setter, name) => { + if (name === 'MixedSetter') { + return; + } + normalized.push({ + name, + title: setter.title || name, + setter: name, + condition: setter.condition, + initialValue: setter.initialValue, + list: setter.recommend || false, + }); + }); + return normalized; + } + const names: string[] = []; + function generateName(n: string) { + let idx = 1; + let got = n; + while (names.indexOf(got) > -1) { + got = `${n}:${idx++}`; + } + names.push(got); + return got; + } + return setters.map((setter) => { + const config: any = { + setter, + list: true, + }; + if (isSetterConfig(setter)) { + config.setter = setter.componentName; + config.props = setter.props; + config.condition = setter.condition; + config.initialValue = setter.initialValue; + config.title = setter.title; + } + if (typeof config.setter === 'string') { + config.name = config.setter; + names.push(config.name); + const info = getSetter(config.setter); + if (!config.title) { + config.title = info?.title || config.setter; + } + if (!config.condition) { + config.condition = info?.condition; + } + if (!config.initialValue) { + config.initialValue = info?.initialValue; + } + } else { + config.name = generateName((config.setter as any).displayName || (config.setter as any).name || 'CustomSetter'); + if (!config.title) { + config.title = config.name; + } + } + return config; + }); +} + +@observer +export default class MixedSetter extends Component<{ + field: SettingField; + setters?: Array<string | SetterConfig | CustomView | DynamicSetter>; + onSetterChange?: (field: SettingField, name: string) => void; + onChange?: (val: any) => void; + value?: any; + className?: string; +}> { + private setters = nomalizeSetters(this.props.setters); + @obx.ref private used?: string; + @computed private getCurrentSetter() { + const { field } = this.props; + let firstMatched: SetterItem | undefined; + for (const setter of this.setters) { + const matched = !setter.condition || setter.condition(field); + if (matched) { + if (setter.name === this.used) { + return setter; + } + if (!firstMatched) { + firstMatched = setter; + } + } + } + return firstMatched; + } + + private useSetter = (name: string) => { + if (name === this.used) { + return; + } + const { field, onChange } = this.props; + const setter = this.setters.find((item) => item.name === name); + this.used = name; + if (setter) { + let newValue: any = setter.initialValue; + if (newValue && typeof newValue === 'function') { + newValue = newValue(field); + } + onChange && onChange(newValue); + } + }; + + private shell: HTMLDivElement | null = null; + private checkIsBlockField() { + if (this.shell) { + const setter = this.shell.firstElementChild; + if (setter && setter.classList.contains('lc-block-setter')) { + this.shell.classList.add('lc-block-setter'); + } else { + this.shell.classList.remove('lc-block-setter'); + } + } + } + componentDidUpdate() { + this.checkIsBlockField(); + } + componentDidMount() { + this.checkIsBlockField(); + } + + render() { + const { className, field, setters, onSetterChange, ...restProps } = this.props; + + const currentSetter = this.getCurrentSetter(); + const isTwoType = this.setters.length < 3; + + let setterContent: any; + const triggerTitle: any = { + tip: { + type: 'i18n', + 'zh-CN': '切换格式', + 'en-US': 'Switch Format', + }, + icon: <IconConvert size={24} />, + }; + if (currentSetter) { + const { setter, title, props } = currentSetter; + let setterProps: any = {}; + let setterType: any; + if (isDynamicSetter(setter)) { + setterType = setter.call(field, field); + } else { + setterType = setter; + } + if (props) { + setterProps = props; + if (typeof setterProps === 'function') { + setterProps = setterProps(field); + } + } + + setterContent = createSetterContent(setterType, { + ...shallowIntl(setterProps), + field, + ...restProps, + }); + if (title) { + if (typeof title !== 'object' || isI18nData(title) || isValidElement(title)) { + triggerTitle.tip = title; + } else { + triggerTitle.tip = title.tip || title.label; + } + } + } else { + // 未匹配的 null 值,显示 NullValue 空值 + // 未匹配的 其它 值,显示 InvalidValue 非法值 + if (restProps.value == null) { + setterContent = <span>NullValue</span>; + } else { + setterContent = <span>InvalidValue</span>; + } + } + const usedName = currentSetter?.name || this.used; + let moreBtnNode = ( + <Title + title={triggerTitle} + className="lc-switch-trigger" + onClick={ + isTwoType + ? () => { + if (this.setters[0]?.name === usedName) { + this.useSetter(this.setters[1]?.name); + } else { + this.useSetter(this.setters[0]?.name); + } + } + : undefined + } + /> + ); + if (!isTwoType) { + moreBtnNode = ( + <Dropdown trigger={moreBtnNode} triggerType="click" align="tr br"> + <Menu selectMode="single" hasSelectedIcon={true} selectedKeys={usedName} onItemClick={this.useSetter}> + {this.setters.filter(setter => setter.list || setter.name === usedName).map((setter) => { + return ( + <Menu.Item key={setter.name}> + <Title title={setter.title} /> + </Menu.Item> + ); + })} + </Menu> + </Dropdown> + ); + } + + return ( + <div ref={(shell) => (this.shell = shell)} className={classNames('lc-setter-mixed', className)}> + {setterContent} + + <div className="lc-setter-actions">{moreBtnNode}</div> + </div> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/mixed-setter/style.less b/packages/vision-polyfill/src/skeleton/components/mixed-setter/style.less new file mode 100644 index 000000000..6efebb28c --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/mixed-setter/style.less @@ -0,0 +1,30 @@ +.lc-setter-mixed { + flex: 1; + min-width: 0; + margin-right: 26px; + display: block; + position: relative; + >.lc-setter-actions { + position: absolute; + right: -2px; + top: 50%; + transform: translate(100%, -50%); + .lc-switch-trigger { + cursor: pointer; + opacity: 0.6; + &:hover { + opacity: 1; + } + } + } + .next-input,.next-date-picker { + width: 100%; + } + &.lc-block-setter { + position: static; + margin-right: 0; + >.lc-setter-actions { + transform: none; + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/object-setter/index.tsx b/packages/vision-polyfill/src/skeleton/components/object-setter/index.tsx new file mode 100644 index 000000000..2f84c8adb --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/object-setter/index.tsx @@ -0,0 +1,180 @@ +import { Component, Fragment } from 'react'; +import { Icon, Button } from '@alifd/next'; +import { Title, SetterType, FieldConfig } from '@ali/lowcode-globals'; +import { createSettingFieldView } from '../../settings/settings-pane'; +import { PopupContext, PopupPipe } from '../../popup'; +import { SettingField } from '@ali/lowcode-designer'; +import './style.less'; + +export default class ObjectSetter extends Component<{ + field: SettingField; + descriptor?: string | ((rowField: SettingField) => string); + config: ObjectSetterConfig; + mode?: 'popup' | 'form'; + // 1: in tablerow 2: in listrow 3: in column-cell + forceInline?: number; +}> { + render() { + const { mode, forceInline = 0, ...props } = this.props; + if (forceInline || mode === 'popup') { + if (forceInline > 2 || mode === 'popup') { + // popup + return <RowSetter {...props} primaryButton={forceInline ? false : true} />; + } else { + return <RowSetter columns={forceInline > 1 ? 2 : 4} {...props} />; + } + } else { + // form + return <FormSetter {...props} />; + } + } +} + +interface ObjectSetterConfig { + items?: FieldConfig[]; + extraSetter?: SetterType; +} + +interface RowSetterProps { + field: SettingField; + descriptor?: string | ((rowField: SettingField) => string); + config: ObjectSetterConfig; + columns?: number; + primaryButton?: boolean; +} + +class RowSetter extends Component<RowSetterProps> { + static contextType = PopupContext; + + state: any = { + descriptor: '', + }; + + private items?: SettingField[]; + constructor(props: RowSetterProps) { + super(props); + const { config, descriptor, field, columns } = props; + const items: SettingField[] = []; + if (columns && config.items) { + const l = Math.min(config.items.length, columns); + for (let i = 0; i < l; i++) { + const conf = config.items[i]; + if (conf.isRequired || conf.important || (conf.setter as any)?.isRequired) { + const item = field.createField({ + ...conf, + // in column-cell + forceInline: 3, + }); + items.push(item); + } + } + } + + if (items.length > 0) { + this.items = items; + } + + let firstRun: boolean = true; + field.onEffect(() => { + let state: any = {}; + if (descriptor) { + if (typeof descriptor === 'function') { + state.descriptor = descriptor(field); + } else { + state.descriptor = field.getPropValue(descriptor); + } + } else { + state.descriptor = field.title; + } + + if (firstRun) { + firstRun = false; + this.state = state; + } else { + this.setState(state); + } + }); + } + + shouldComponentUpdate(_: any, nextState: any) { + if (this.state.decriptor !== nextState.decriptor) { + return true; + } + return false; + } + + private pipe: any; + render() { + const items = this.items; + const { field, primaryButton, config } = this.props; + + if (!this.pipe) { + this.pipe = (this.context as PopupPipe).create({ width: 320 }); + } + + const title = ( + <Fragment> + 编辑: + <Title title={this.state.descriptor} /> + </Fragment> + ); + + this.pipe.send(<FormSetter key={field.id} field={field} config={config} />, title); + + if (items) { + return ( + <div className="lc-setter-object-row"> + <div + className="lc-setter-object-row-edit" + onClick={(e) => { + this.pipe.show((e as any).target, field.id); + }} + > + <Icon size="small" type="edit" /> + </div> + <div className="lc-setter-object-row-body">{items.map((item) => createSettingFieldView(item, field))}</div> + </div> + ); + } + + return ( + <Button + type={primaryButton === false ? 'normal' : 'primary'} + onClick={(e) => { + this.pipe.show((e as any).target, field.id); + }} + > + <Icon type="edit" /> + {title} + </Button> + ); + } +} + +interface FormSetterProps { + field: SettingField; + config: ObjectSetterConfig; +} +class FormSetter extends Component<FormSetterProps> { + private items: SettingField[]; + constructor(props: RowSetterProps) { + super(props); + const { config, field } = props; + this.items = (config.items || []).map((conf) => field.createField(conf)); + + // TODO: extraConfig for custom fields + } + + shouldComponentUpdate() { + return false; + } + + render() { + const { field } = this.props; + return ( + <div className="lc-setter-object lc-block-setter"> + {this.items.map((item, index) => createSettingFieldView(item, field, index))} + </div> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/object-setter/style.less b/packages/vision-polyfill/src/skeleton/components/object-setter/style.less new file mode 100644 index 000000000..6d6064df1 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/object-setter/style.less @@ -0,0 +1,31 @@ +.lc-setter-object-row { + display: flex; + align-items: stretch; + width: 100%; + .lc-setter-object-row-edit { + width: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + .lc-setter-object-row-body { + display: flex; + flex: 1; + min-width: 0; + align-items: center; + .lc-field { + padding: 0 !important; + .lc-field-body { + padding: 0 !important; margin: 0 !important; + } + } + > * { + flex: 1; + flex-shrink: 1; + margin-left: 2px; + min-width: 0; + overflow: hidden; + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/popup/index.tsx b/packages/vision-polyfill/src/skeleton/components/popup/index.tsx new file mode 100644 index 000000000..503e986f9 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/popup/index.tsx @@ -0,0 +1,150 @@ +import { createContext, ReactNode, Component, PureComponent } from 'react'; +import { EventEmitter } from 'events'; +import { Balloon } from '@alifd/next'; +import { uniqueId } from '@ali/lowcode-globals'; +import './style.less'; + +export const PopupContext = createContext<PopupPipe>({} as any); + +export class PopupPipe { + private emitter = new EventEmitter(); + private currentId?: string; + + create(props?: object): { send: (content: ReactNode, title: ReactNode) => void; show: (target: Element) => void } { + let sendContent: ReactNode = null; + let sendTitle: ReactNode = null; + const id = uniqueId('popup'); + return { + send: (content: ReactNode, title: ReactNode) => { + sendContent = content; + sendTitle = title; + if (this.currentId === id) { + this.popup({ + ...props, + content, + title, + }); + } + }, + show: (target: Element, actionKey?: string) => { + this.currentId = id; + this.popup( + { + ...props, + actionKey, + content: sendContent, + title: sendTitle, + }, + target, + ); + }, + }; + } + + private popup(props: object, target?: Element) { + Promise.resolve().then(() => { + this.emitter.emit('popupchange', props, target); + }); + } + + onPopupChange(fn: (props: object, target?: Element) => void): () => void { + this.emitter.on('popupchange', fn); + return () => { + this.emitter.removeListener('popupchange', fn); + }; + } + + purge() { + this.emitter.removeAllListeners(); + } +} + +export default class PopupService extends Component<{ actionKey?: string; safeId?: string }> { + private popupPipe = new PopupPipe(); + + componentWillUnmount() { + this.popupPipe.purge(); + } + + render() { + const { children, actionKey, safeId } = this.props; + return ( + <PopupContext.Provider value={this.popupPipe}> + {children} + <PopupContent key={'pop' + actionKey} safeId={safeId} /> + </PopupContext.Provider> + ); + } +} + +export class PopupContent extends PureComponent<{ safeId?: string }> { + static contextType = PopupContext; + state: any = { + visible: false, + pos: {}, + }; + + private dispose = (this.context as PopupPipe).onPopupChange((props, target) => { + const state: any = { + ...props, + visible: true, + }; + if (target) { + const rect = target.getBoundingClientRect(); + state.pos = { + top: rect.top, + height: rect.height, + }; + // todo: compute the align method + } + this.setState(state); + }); + + componentWillUnmount() { + this.dispose(); + } + + render() { + const { content, visible, width, title, pos, actionKey } = this.state; + if (!visible) { + return null; + } + let avoidLaterHidden = true; + setTimeout(() => { + avoidLaterHidden = false; + }, 10); + + const id = uniqueId('ball'); + + return ( + <Balloon + className="lc-ballon" + align="l" + id={this.props.safeId} + safeNode={id} + visible={visible} + style={{ width }} + onVisibleChange={(visible) => { + if (avoidLaterHidden) { + return; + } + if (!visible) { + this.setState({ visible: false }); + } + }} + trigger={<div className="lc-popup-placeholder" style={pos} />} + triggerType="click" + animation={false} + // needAdjust + shouldUpdatePosition + > + <div className="lc-ballon-title">{title}</div> + <div className="lc-ballon-content"> + <PopupService actionKey={actionKey} safeId={id}> + {content} + </PopupService> + </div> + </Balloon> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/popup/style.less b/packages/vision-polyfill/src/skeleton/components/popup/style.less new file mode 100644 index 000000000..b115fd371 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/popup/style.less @@ -0,0 +1,22 @@ +.lc-popup-placeholder { + position: fixed; + width: 100%; + pointer-events: none; +} + +.lc-ballon { + padding: 10px; + max-width: 640px; + width: 640px; + .lc-ballon-title { + font-size: 14px; + } + .lc-ballon-content { + margin-top: 10px; + // width: 300px; + } + .next-balloon-close { + top: 4px; + right: 4px; + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/register.ts b/packages/vision-polyfill/src/skeleton/components/register.ts new file mode 100644 index 000000000..3c096aa82 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/register.ts @@ -0,0 +1,29 @@ +import { registerSetter, isPlainObject } from '@ali/lowcode-globals'; +import ArraySetter from './array-setter'; +import ObjectSetter from './settings-pane/src/setters/object-setter'; +import MixedSetter from './mixed-setter'; + +registerSetter('ArraySetter', { + component: ArraySetter, + defaultProps: {}, + title: 'ArraySetter', // TODO + condition: (field: any) => { + const v = field.getValue(); + return v == null || Array.isArray(v); + }, + initialValue: [], + recommend: true, +}); +registerSetter('ObjectSetter', { + component: ObjectSetter, + // todo: defaultProps + defaultProps: {}, + title: 'ObjectSetter', // TODO + condition: (field: any) => { + const v = field.getValue(); + return v == null || isPlainObject(v); + }, + initialValue: {}, + recommend: true, +}); +registerSetter('MixedSetter', MixedSetter); diff --git a/packages/vision-polyfill/src/skeleton/components/settings/index.ts b/packages/vision-polyfill/src/skeleton/components/settings/index.ts new file mode 100644 index 000000000..f51b7b704 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/index.ts @@ -0,0 +1,9 @@ +import { createSettingFieldView } from './settings-pane'; +import './transducers/register'; +import '../../register'; +import './style.less'; +import SettingsMainView from './settings-primary-view'; + +export default SettingsMainView; + +export { createSettingFieldView }; diff --git a/packages/vision-polyfill/src/skeleton/components/settings/main.ts b/packages/vision-polyfill/src/skeleton/components/settings/main.ts new file mode 100644 index 000000000..f752add28 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/main.ts @@ -0,0 +1,89 @@ +import { EventEmitter } from 'events'; +import { obx, computed } from '@ali/lowcode-globals'; +import { Node, Designer, Selection, SettingTopEntry } from '@ali/lowcode-designer'; +import { getTreeMaster } from '@ali/lowcode-plugin-outline-pane'; +import Editor from '@ali/lowcode-editor-core'; + +function generateSessionId(nodes: Node[]) { + return nodes + .map((node) => node.id) + .sort() + .join(','); +} + +export class SettingsMain { + private emitter = new EventEmitter(); + private _sessionId = ''; + @obx.ref private _settings?: SettingTopEntry; + + @computed get length(): number | undefined { + return this._settings?.nodes.length; + } + + @computed get componentMeta() { + return this._settings?.componentMeta; + } + + get settings() { + return this._settings; + } + + private disposeListener: () => void; + + private designer?: Designer; + + constructor(readonly editor: Editor) { + this.init(); + } + + private async init() { + const setupSelection = (selection?: Selection) => { + if (selection) { + this.setup(selection.getNodes()); + } else { + this.setup([]); + } + }; + this.editor.on('designer.selection.change', setupSelection); + this.disposeListener = () => { + this.editor.removeListener('designer.selection.change', setupSelection); + }; + const designer = await this.editor.onceGot(Designer); + this.designer = designer; + getTreeMaster(designer).onceEnableBuiltin(() => { + this.emitter.emit('outline-visible'); + }); + setupSelection(designer.currentSelection); + } + + private setup(nodes: Node[]) { + // check nodes change + const sessionId = generateSessionId(nodes); + if (sessionId === this._sessionId) { + return; + } + this._sessionId = sessionId; + if (nodes.length < 1) { + this._settings = undefined; + return; + } + + if (!this.designer) { + this.designer = nodes[0].document.designer; + } + + this._settings = this.designer.createSettingEntry(this.editor, nodes); + } + + onceOutlineVisible(fn: () => void): () => void { + this.emitter.on('outline-visible', fn); + return () => { + this.emitter.removeListener('outline-visible', fn); + }; + } + + purge() { + this.disposeListener(); + this.emitter.removeAllListeners(); + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/settings/package.json b/packages/vision-polyfill/src/skeleton/components/settings/package.json new file mode 100644 index 000000000..2a0fab24e --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/package.json @@ -0,0 +1,49 @@ +{ + "name": "@ali/lowcode-plugin-settings-pane", + "version": "0.8.10", + "description": "Settings pane for Ali lowCode engine", + "files": [ + "es", + "lib" + ], + "main": "lib/index.js", + "module": "es/index.js", + "scripts": { + "build": "build-scripts build --skip-demo", + "test": "ava", + "test:snapshot": "ava --update-snapshots" + }, + "dependencies": { + "@ali/lowcode-designer": "^0.9.2", + "@ali/lowcode-editor-core": "^0.8.5", + "@ali/lowcode-globals": "^0.9.2", + "@ali/lowcode-plugin-outline-pane": "^0.8.8", + "@ali/ve-stage-box": "^4.0.0", + "@alifd/next": "^1.19.16", + "classnames": "^2.2.6", + "react": "^16" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.18", + "@types/classnames": "^2.2.7", + "@types/node": "^13.7.1", + "@types/react": "^16", + "build-plugin-component": "^0.2.10", + "build-plugin-fusion": "^0.1.1", + "build-plugin-moment-locales": "^0.1.0" + }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + }, + "license": "MIT", + "publishConfig": { + "registry": "https://registry.npm.alibaba-inc.com" + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/settings/settings-pane.tsx b/packages/vision-polyfill/src/skeleton/components/settings/settings-pane.tsx new file mode 100644 index 000000000..11edc8a4c --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/settings-pane.tsx @@ -0,0 +1,149 @@ +import { Component } from 'react'; +import { + createContent, + CustomView, + intl, + shallowIntl, + isSetterConfig, + createSetterContent, + observer, +} from '@ali/lowcode-globals'; +import { Field, createField } from '../field'; +import PopupService from '../popup'; +import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer'; + +@observer +class SettingFieldView extends Component<{ field: SettingField }> { + render() { + const { field } = this.props; + const { extraProps } = field; + const { condition, defaultValue } = extraProps; + const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; + if (!visible) { + return null; + } + const { setter } = field; + + let setterProps: any = {}; + let setterType: any; + if (Array.isArray(setter)) { + setterType = 'MixedSetter'; + setterProps = { + setters: setter, + }; + } else if (isSetterConfig(setter)) { + setterType = setter.componentName; + if (setter.props) { + setterProps = setter.props; + if (typeof setterProps === 'function') { + setterProps = setterProps(field); + } + } + } else if (setter) { + setterType = setter; + } + let value = null; + if (field.type === 'field') { + if (defaultValue != null && !('defaultValue' in setterProps)) { + setterProps.defaultValue = defaultValue; + } + if (field.valueState > 0) { + value = field.getValue(); + } else { + setterProps.multiValue = true; + if (!('placeholder' in setterProps)) { + // FIXME! move to locale file + setterProps.placeholder = intl({ + type: 'i18n', + 'zh-CN': '多种值', + 'en-US': 'Multiple Value', + }); + } + } + } + + // todo: error handling + + return createField({ + title: field.title, + collapsed: !field.expanded, + onExpandChange: (expandState) => field.setExpanded(expandState), + }, createSetterContent(setterType, { + ...shallowIntl(setterProps), + forceInline: extraProps.forceInline, + key: field.id, + // === injection + prop: field, // for compatible vision + field, + // === IO + value, // reaction point + onChange: (value: any) => { + this.setState({ + value, + }); + field.setValue(value); + }, + }), extraProps.forceInline ? 'plain' : extraProps.display); + } +} + +@observer +class SettingGroupView extends Component<{ field: SettingField }> { + shouldComponentUpdate() { + return false; + } + + render() { + const { field } = this.props; + const { extraProps } = field; + const { condition } = extraProps; + const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true; + + if (!visible) { + return null; + } + + // todo: split collapsed state | field.items for optimize + return ( + <Field defaultDisplay="accordion" title={field.title} collapsed={!field.expanded} onExpandChange={(expandState) => { + field.setExpanded(expandState); + }}> + {field.items.map((item, index) => createSettingFieldView(item, field, index))} + </Field> + ); + } +} + +export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) { + if (isSettingField(item)) { + if (item.isGroup) { + return <SettingGroupView field={item} key={item.id} />; + } else { + return <SettingFieldView field={item} key={item.id} />; + } + } else { + return createContent(item, { key: index, field }); + } +} + +@observer +export default class SettingsPane extends Component<{ target: SettingTopEntry | SettingField }> { + shouldComponentUpdate() { + return false; + } + + render() { + const { target } = this.props; + const items = target.items + return ( + <div className="lc-settings-pane"> + {/* todo: add head for single use */} + <PopupService> + <div className="lc-settings-content"> + {items.map((item, index) => createSettingFieldView(item, target, index))} + </div> + </PopupService> + </div> + ); + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/settings/settings-primary-view.tsx b/packages/vision-polyfill/src/skeleton/components/settings/settings-primary-view.tsx new file mode 100644 index 000000000..caf45bc22 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/settings-primary-view.tsx @@ -0,0 +1,121 @@ +import React, { Component, PureComponent } from 'react'; +import { Tab, Breadcrumb } from '@alifd/next'; +import { Title, createIcon, observer } from '@ali/lowcode-globals'; +import { Node, isSettingField, SettingField } from '@ali/lowcode-designer'; +import Editor from '@ali/lowcode-editor-core'; +import { SettingsMain } from './main'; +import SettingsPane from './settings-pane'; + +@observer +export default class SettingsMainView extends Component<{ editor: Editor }> { + private main = new SettingsMain(this.props.editor); + + shouldComponentUpdate() { + return false; + } + + componentWillUnmount() { + this.main.purge(); + } + + renderBreadcrumb() { + const { settings } = this.main; + if (!settings) { + return null; + } + if (settings.isMultiple) { + return ( + <div className="lc-settings-navigator"> + {createIcon(settings.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})} + <Title title={settings.componentMeta!.title} /> + <span>x {settings.nodes.length}</span> + </div> + ); + } + + let node: Node | null = settings.first; + const items = []; + let l = 3; + while (l-- > 0 && node) { + const props = + l === 2 + ? {} + : { + onMouseOver: hoverNode.bind(null, node, true), + onMouseOut: hoverNode.bind(null, node, false), + onClick: selectNode.bind(null, node), + }; + items.unshift(<Breadcrumb.Item {...props} key={node.id}><Title title={node.title} /></Breadcrumb.Item>); + node = node.parent; + } + + return ( + <div className="lc-settings-navigator"> + {createIcon(this.main.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})} + <Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb> + </div> + ); + } + + render() { + const { settings } = this.main; + if (!settings) { + // 未选中节点,提示选中 或者 显示根节点设置 + return ( + <div className="lc-settings-main"> + <div className="lc-settings-notice"> + <p>请在左侧画布选中节点</p> + </div> + </div> + ); + } + + if (!settings.isSameComponent) { + // todo: future support 获取设置项交集编辑 + return ( + <div className="lc-settings-main"> + <div className="lc-settings-notice"> + <p>请选中同一类型节点编辑</p> + </div> + </div> + ); + } + + const { items } = settings; + if (items.length > 5 || items.some(item => !isSettingField(item) || !item.isGroup)) { + return ( + <div className="lc-settings-main"> + {this.renderBreadcrumb()} + <div className="lc-settings-body"> + <SettingsPane target={settings} /> + </div> + </div> + ); + } + + return ( + <div className="lc-settings-main"> + <Tab + navClassName="lc-settings-tabs" + animation={false} + excessMode="dropdown" + contentClassName="lc-settings-tabs-content" + extra={this.renderBreadcrumb()} + > + {(items as SettingField[]).map(field => ( + <Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}> + <SettingsPane target={field} key={field.id} /> + </Tab.Item> + ))} + </Tab> + </div> + ); + } +} + +function hoverNode(node: Node, flag: boolean) { + node.hover(flag); +} +function selectNode(node: Node) { + node.select(); +} diff --git a/packages/vision-polyfill/src/skeleton/components/settings/style.less b/packages/vision-polyfill/src/skeleton/components/settings/style.less new file mode 100644 index 000000000..9b2c2b403 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/style.less @@ -0,0 +1,124 @@ +.lc-settings-main { + position: relative; + height: 100%; + overflow: hidden; + + .lc-settings-notice { + text-align: center; + font-size: 12px; + font-family: PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica,Arial,sans-serif; + color: var(--color-text ,rgba(0,0,0,.6)); + padding: 50px 15px 0; + } + + .lc-settings-navigator { + height: 30px; + display: flex; + align-items: center; + padding-left: 5px; + border-bottom: 1px solid var(--color-line-normal); + .lc-settings-navigator-icon { + width: 16px; + height: 16px; + * { + fill: var(--color-icon-normal, rgba(31, 56, 88, 0.4)); + } + } + .lc-settings-node-breadcrumb { + margin-left: 5px; + .next-breadcrumb { + display: inline-flex; + align-items: stretch; + height: 24px; + } + .next-breadcrumb-item { + display: inline-flex; + align-items: center; + cursor: default; + &:not(:last-child):hover { + cursor: pointer; + } + .next-breadcrumb-text { + font-size: 12px; + } + } + } + } + + .lc-settings-body { + position: absolute; + top: 30px; + right: 0; + left: 0; + bottom: 0; + overflow-y: auto; + } + + // ====== reset fusion-tabs ===== + .lc-settings-tabs { + position: relative; + overflow: visible; + > .next-tabs-nav-extra { + position: absolute !important; + top: 40px !important; + left: 0 !important; + height: 30px; + right: 0; + transform: none !important; + + } + .next-tabs-nav-container { + .next-tabs-nav { + display: flex; + .next-tabs-tab.lc-settings-tab-item { + flex: 1; + min-width: 0; + outline: none; + .next-tabs-tab-inner { + text-align: center; + padding: 12px 0; + } + } + } + } + } + + .lc-settings-tabs-content { + position: absolute; + top: 70px; + left:0; + right: 0; + bottom: 0; + .next-tabs-tabpane { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + overflow-y: auto; + outline: none !important; + box-shadow: none !important; + } + } + .lc-outline-pane { + position: absolute; + z-index: 100; + background-color: white; + top: 0; + bottom: 0; + display: none; + } +} + +.lc-settings-pane { + padding-bottom: 50px; + .next-btn { + line-height: 1 !important; + } +} + +html.lc-cursor-dragging:not(.lowcode-has-fixed-tree) { + .lc-settings-main .lc-outline-pane { + display: block; + } +} diff --git a/packages/vision-polyfill/src/skeleton/components/settings/utils.js b/packages/vision-polyfill/src/skeleton/components/settings/utils.js new file mode 100644 index 000000000..b8f5bbdcc --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/components/settings/utils.js @@ -0,0 +1,41 @@ +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 +} + +function combineTransducer(transducer, arr, context) { + if (!transducer && Array.isArray(arr)) { + const [toHot, toNative] = arr; + transducer = { toHot, toNative }; + } + + return { + toHot: (transducer && transducer.toHot || (x => x)).bind(context), // eslint-disable-line + toNative: (transducer && transducer.toNative || (x => x)).bind(context), // eslint-disable-line + }; +} + +export class Transducer { + constructor(context, config) { + this.setterTransducer = combineTransducer( + getTransducerFromSetter(config.setter), + getHotterFromSetter(config.setter), + context, + ); + this.context = context; + } + + toHot(data) { + return this.setterTransducer.toHot(data); + } + + toNative(data) { + return this.setterTransducer.toNative(data); + } +} diff --git a/packages/vision-polyfill/src/skeleton/widget-views.tsx b/packages/vision-polyfill/src/skeleton/components/widget-views.tsx similarity index 94% rename from packages/vision-polyfill/src/skeleton/widget-views.tsx rename to packages/vision-polyfill/src/skeleton/components/widget-views.tsx index 6f8037aa3..6d32600b1 100644 --- a/packages/vision-polyfill/src/skeleton/widget-views.tsx +++ b/packages/vision-polyfill/src/skeleton/components/widget-views.tsx @@ -1,13 +1,13 @@ 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 { IWidget } from './widget'; -import { SkeletonEvents } from './skeleton'; +import { DockProps } from '../types'; +import PanelDock from '../widget/panel-dock'; +import { composeTitle } from '../widget/utils'; +import WidgetContainer from '../widget/widget-container'; +import Panel from '../widget/panel'; +import { IWidget } from '../widget/widget'; +import { SkeletonEvents } from '../skeleton'; export function DockView({ title, icon, description, size, className, onClick }: DockProps) { return ( diff --git a/packages/vision-polyfill/src/skeleton/dock.ts b/packages/vision-polyfill/src/skeleton/dock.ts index 5fd726e41..721c6b9c6 100644 --- a/packages/vision-polyfill/src/skeleton/dock.ts +++ b/packages/vision-polyfill/src/skeleton/dock.ts @@ -2,8 +2,8 @@ import { ReactNode, createElement } from 'react'; import { uniqueId, createContent, obx } from '@ali/lowcode-globals'; import { DockConfig } from "./types"; import { Skeleton } from './skeleton'; -import { DockView, WidgetView } from './widget-views'; -import { IWidget } from './widget'; +import { DockView, WidgetView } from './components/widget-views'; +import { IWidget } from './widget/widget'; /** * 带图标(主要)/标题(次要)的扩展 diff --git a/packages/vision-polyfill/src/skeleton/icons/convert.tsx b/packages/vision-polyfill/src/skeleton/icons/convert.tsx new file mode 100644 index 000000000..71ab8630e --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/icons/convert.tsx @@ -0,0 +1,16 @@ +import { SVGIcon, IconProps } from "@ali/lowcode-globals"; + +export function IconConvert(props: IconProps) { + return ( + <SVGIcon viewBox="0 0 1024 1024" {...props}> + <path d="M508.16 889.6C291.84 889.6 115.2 714.24 115.2 497.92 115.2 281.6 291.84 106.24 509.44 106.24c43.52 0 85.76 6.4 124.16 20.48l-10.24 30.72c-35.84-11.52-72.96-17.92-113.92-17.92-199.68 0-362.24 161.28-362.24 359.68s162.56 358.4 360.96 358.4 359.68-161.28 359.68-359.68c0-66.56-17.92-131.84-51.2-185.6L844.8 294.4c37.12 60.16 56.32 130.56 56.32 203.52-1.28 216.32-176.64 391.68-392.96 391.68z" /> + <path d="M627.2 140.8m-15.36 0a15.36 15.36 0 1 0 30.72 0 15.36 15.36 0 1 0-30.72 0Z" /> + <path d="M832 304.64m-15.36 0a15.36 15.36 0 1 0 30.72 0 15.36 15.36 0 1 0-30.72 0Z" /> + <path d="M348.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" /> + <path d="M508.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" /> + <path d="M668.16 497.92m-35.84 0a35.84 35.84 0 1 0 71.68 0 35.84 35.84 0 1 0-71.68 0Z" fill="#35A2D4" /> + </SVGIcon> + ); +} + +IconConvert.displayName = 'Convert'; diff --git a/packages/vision-polyfill/src/skeleton/index.ts b/packages/vision-polyfill/src/skeleton/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/vision-polyfill/src/skeleton/bottom-area.tsx b/packages/vision-polyfill/src/skeleton/layouts/bottom-area.tsx similarity index 92% rename from packages/vision-polyfill/src/skeleton/bottom-area.tsx rename to packages/vision-polyfill/src/skeleton/layouts/bottom-area.tsx index 89834b0c2..ae251cd2e 100644 --- a/packages/vision-polyfill/src/skeleton/bottom-area.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/bottom-area.tsx @@ -1,8 +1,8 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; -import Area from './area'; -import Panel from './panel'; +import Area from '../area'; +import Panel from '../widget/panel'; @observer export default class BottomArea extends Component<{ area: Area<any, Panel> }> { diff --git a/packages/vision-polyfill/src/skeleton/left-area.tsx b/packages/vision-polyfill/src/skeleton/layouts/left-area.tsx similarity index 97% rename from packages/vision-polyfill/src/skeleton/left-area.tsx rename to packages/vision-polyfill/src/skeleton/layouts/left-area.tsx index 87df6170c..8fe785055 100644 --- a/packages/vision-polyfill/src/skeleton/left-area.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/left-area.tsx @@ -1,7 +1,7 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; -import Area from './area'; +import Area from '../area'; @observer export default class LeftArea extends Component<{ area: Area }> { diff --git a/packages/vision-polyfill/src/skeleton/left-fixed-pane.tsx b/packages/vision-polyfill/src/skeleton/layouts/left-fixed-pane.tsx similarity index 91% rename from packages/vision-polyfill/src/skeleton/left-fixed-pane.tsx rename to packages/vision-polyfill/src/skeleton/layouts/left-fixed-pane.tsx index 89ccc37a7..bae3a9eae 100644 --- a/packages/vision-polyfill/src/skeleton/left-fixed-pane.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/left-fixed-pane.tsx @@ -2,9 +2,9 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; import { Button, Icon } from '@alifd/next'; -import Area from './area'; -import { PanelConfig } from './types'; -import Panel from './panel'; +import Area from '../area'; +import { PanelConfig } from '../types'; +import Panel from '../widget/panel'; @observer export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, Panel> }> { diff --git a/packages/vision-polyfill/src/skeleton/left-float-pane.tsx b/packages/vision-polyfill/src/skeleton/layouts/left-float-pane.tsx similarity index 96% rename from packages/vision-polyfill/src/skeleton/left-float-pane.tsx rename to packages/vision-polyfill/src/skeleton/layouts/left-float-pane.tsx index a38433a0e..fefe6d525 100644 --- a/packages/vision-polyfill/src/skeleton/left-float-pane.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/left-float-pane.tsx @@ -2,8 +2,8 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; import { Button, Icon } from '@alifd/next'; -import Area from './area'; -import Panel from './panel'; +import Area from '../area'; +import Panel from '../widget/panel'; @observer export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> { diff --git a/packages/vision-polyfill/src/skeleton/main-area.tsx b/packages/vision-polyfill/src/skeleton/layouts/main-area.tsx similarity index 81% rename from packages/vision-polyfill/src/skeleton/main-area.tsx rename to packages/vision-polyfill/src/skeleton/layouts/main-area.tsx index 45242805e..a531398b0 100644 --- a/packages/vision-polyfill/src/skeleton/main-area.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/main-area.tsx @@ -1,9 +1,9 @@ import { Component } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; -import Area from './area'; -import Panel from './panel'; -import Widget from './widget'; +import Area from '../area'; +import Panel from '../widget/panel'; +import Widget from '../widget/widget'; @observer export default class MainArea extends Component<{ area: Area<any, Panel | Widget> }> { diff --git a/packages/vision-polyfill/src/skeleton/right-area.tsx b/packages/vision-polyfill/src/skeleton/layouts/right-area.tsx similarity index 91% rename from packages/vision-polyfill/src/skeleton/right-area.tsx rename to packages/vision-polyfill/src/skeleton/layouts/right-area.tsx index 89daa1ea2..b27223f1f 100644 --- a/packages/vision-polyfill/src/skeleton/right-area.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/right-area.tsx @@ -1,8 +1,8 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; -import Area from './area'; -import Panel from './panel'; +import Area from '../area'; +import Panel from '../widget/panel'; @observer export default class RightArea extends Component<{ area: Area<any, Panel> }> { diff --git a/packages/vision-polyfill/src/skeleton/layouts/theme.less b/packages/vision-polyfill/src/skeleton/layouts/theme.less new file mode 100644 index 000000000..5171884c0 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/layouts/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/layouts/toolbar.tsx similarity index 97% rename from packages/vision-polyfill/src/skeleton/toolbar.tsx rename to packages/vision-polyfill/src/skeleton/layouts/toolbar.tsx index 7a0c7561a..ea9c02ef0 100644 --- a/packages/vision-polyfill/src/skeleton/toolbar.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/toolbar.tsx @@ -1,7 +1,7 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; -import Area from './area'; +import Area from '../area'; @observer export default class Toolbar extends Component<{ area: Area }> { diff --git a/packages/vision-polyfill/src/skeleton/top-area.tsx b/packages/vision-polyfill/src/skeleton/layouts/top-area.tsx similarity index 97% rename from packages/vision-polyfill/src/skeleton/top-area.tsx rename to packages/vision-polyfill/src/skeleton/layouts/top-area.tsx index d1c3812d2..13da4d17e 100644 --- a/packages/vision-polyfill/src/skeleton/top-area.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/top-area.tsx @@ -1,7 +1,7 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@ali/lowcode-globals'; -import Area from './area'; +import Area from '../area'; @observer export default class TopArea extends Component<{ area: Area }> { diff --git a/packages/vision-polyfill/src/skeleton/layouts/workbench.less b/packages/vision-polyfill/src/skeleton/layouts/workbench.less new file mode 100644 index 000000000..92deeeacd --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/layouts/workbench.less @@ -0,0 +1,375 @@ +@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: 48px; + --right-area-width: 280px; + --top-area-height: 48px; + --toolbar-height: 36px; + --dock-pane-width: 280px; + --dock-fixed-pane-width: 280px; +} + +@media (min-width: 1860px) { + :root { + --right-area-width: 400px; + --dock-pane-width: 452px; + --dock-fixed-pane-width: 350px; + } +} + +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; +} + + +.lc-titled-panel { + 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 { + 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; + padding: 8px; + .lc-top-area-left{} + .lc-top-area-center{ + flex: 1; + display: flex; + justify-content: flex-end; + margin-right: 8px; + } + .lc-top-area-right{ + display: flex; + align-items: center; + >* { + margin-left: 4px; + margin-right: 4px; + } + } + } + .lc-workbench-body { + flex: 1; + display: flex; + min-height: 0; + position: relative; + .lc-left-float-pane { + position: absolute; + top: 0; + bottom: 0; + width: var(--dock-pane-width); + left: calc(var(--left-area-width) + 1px); + background-color: var(--color-pane-background); + box-shadow: 4px 0 16px 0 rgba(31,50,88,0.08); + z-index: 820; + display: none; + // padding-top: 36px; + &.lc-area-visible { + display: block; + } + .lc-pane-close{ + position: absolute; + right: 10px; + top: 6px; + z-index: 2; + .next-icon{ + line-height: 1; + } + } + .lc-tabs-title { + width: 100%; + height: 36px; + position: relative; + display: center; + display: flex; + justify-content: center; + align-items: center; + background: rgba(31,56,88,0.04); + } + .lc-tabs-content { + position: absolute; + top: 36px; + bottom: 0; + left: 0; + right: 0; + } + } + .lc-left-area { + height: 100%; + width: var(--left-area-width); + background-color: var(--color-pane-background); + display: none; + flex-shrink: 0; + flex-direction: column; + justify-content: space-between; + &.lc-area-visible { + display: flex; + } + .lc-left-area-top, + .lc-left-area-bottom{ + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + .lc-title{ + padding: 12px; + flex-direction: column; + &.has-tip{ + cursor: pointer; + } + &.actived{ + color: #0079F2; + } + .lc-title-icon{ + margin: 0; + .next-icon:before { + line-height: 1 !important; + } + } + } + } + .lc-left-area-top{ + padding-top: 12px; + } + .lc-left-area-bottom{ + padding-bottom: 12px; + } + } + .lc-left-fixed-pane { + width: var(--dock-fixed-pane-width); + background-color: var(--color-pane-background); + height: 100%; + display: none; + flex-shrink: 0; + position: relative; + &.lc-area-visible { + display: block; + } + .lc-pane-close { + position: absolute; + right: 10px; + top: 6px; + z-index: 2; + .next-icon { + line-height: 1; + } + } + } + .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-outline-pane{ + .lc-outline-tree .tree-node .tree-node-title{ + border-bottom: none; + } + } + .lc-workbench-center { + flex: 1; + display: flex; + flex-direction: column; + .lc-toolbar { + height: var(--toolbar-height); + background-color: var(--color-pane-background); + padding: 8px 16px; + } + .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; + flex-shrink: 0; + margin-left: 2px; + &.lc-area-visible { + display: block; + } + .lc-settings-tabs{ + > .next-tabs-nav-extra{ + top: 36px !important; + } + .lc-settings-tab-item{ + .next-tabs-tab-inner{ + font-size: 12px; + line-height: 12px; + } + } + .lc-title{ + color: inherit; + line-height: inherit !important; + } + } + .lc-settings-tabs-content{ + top: 66px; + } + } + } +} diff --git a/packages/vision-polyfill/src/skeleton/workbench.tsx b/packages/vision-polyfill/src/skeleton/layouts/workbench.tsx similarity index 96% rename from packages/vision-polyfill/src/skeleton/workbench.tsx rename to packages/vision-polyfill/src/skeleton/layouts/workbench.tsx index aa92a2ae6..c55633f39 100644 --- a/packages/vision-polyfill/src/skeleton/workbench.tsx +++ b/packages/vision-polyfill/src/skeleton/layouts/workbench.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; import { TipContainer, observer } from '@ali/lowcode-globals'; -import { Skeleton } from './skeleton'; +import { Skeleton } from '../skeleton'; import TopArea from './top-area'; import LeftArea from './left-area'; import LeftFixedPane from './left-fixed-pane'; diff --git a/packages/vision-polyfill/src/skeleton/panel.ts b/packages/vision-polyfill/src/skeleton/panel.ts index 3a2da4def..c9e2e890d 100644 --- a/packages/vision-polyfill/src/skeleton/panel.ts +++ b/packages/vision-polyfill/src/skeleton/panel.ts @@ -2,9 +2,9 @@ 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 { TitledPanelView, TabsPanelView, PanelView } from './widget-views'; +import { TitledPanelView, TabsPanelView, PanelView } from './components/widget-views'; import { Skeleton } from './skeleton'; -import { composeTitle } from './utils'; +import { composeTitle } from './widget/utils'; import { IWidget } from './widget'; export default class Panel implements IWidget { diff --git a/packages/vision-polyfill/src/skeleton/skeleton.ts b/packages/vision-polyfill/src/skeleton/skeleton.ts index a5627157c..2ed0c7b39 100644 --- a/packages/vision-polyfill/src/skeleton/skeleton.ts +++ b/packages/vision-polyfill/src/skeleton/skeleton.ts @@ -9,16 +9,19 @@ import { isDockConfig, isPanelDockConfig, isPanelConfig, + DividerConfig, + isDividerConfig } from './types'; -import Panel, { isPanel } from './panel'; -import WidgetContainer from './widget-container'; +import Panel, { isPanel } from './widget/panel'; +import WidgetContainer from './widget/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'; +import Widget, { isWidget, IWidget } from './widget/widget'; +import PanelDock from './widget/panel-dock'; +import Dock from './widget/dock'; +import { Stage, StageConfig } from './widget/stage'; import { isValidElement } from 'react'; import { isPlainObject } from 'globals/src/utils'; +import { Divider } from '@alifd/next'; export enum SkeletonEvents { PANEL_DOCK_ACTIVE = 'skeleton.panel-dock.active', @@ -33,8 +36,8 @@ export class Skeleton { private panels = new Map<string, Panel>(); private containers = new Map<string, WidgetContainer<any>>(); readonly leftArea: Area<DockConfig | PanelDockConfig | DialogDockConfig>; - readonly topArea: Area<DockConfig | PanelDockConfig | DialogDockConfig>; - readonly toolbar: Area<DockConfig | PanelDockConfig | DialogDockConfig>; + readonly topArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; readonly leftFixedArea: Area<PanelConfig, Panel>; readonly leftFloatArea: Area<PanelConfig, Panel>; readonly rightArea: Area<PanelConfig, Panel>; @@ -144,7 +147,7 @@ export class Skeleton { } private setupPlugins() { - const { config, componentsMap } = this.editor; + const { config, components: componentsMap } = this.editor; const { plugins } = config; if (!plugins) { return; @@ -200,18 +203,23 @@ export class Skeleton { if (isPanelDockConfig(config)) { widget = new PanelDock(this, config); } else if (false) { + // DialogDock // others... } else { widget = new Dock(this, config); } + } else if (isDividerConfig(config)) { + widget = new Widget(this, { + ...config, + type: 'Widget', + content: Divider, + }); } else if (isPanelConfig(config)) { widget = this.createPanel(config); } else { widget = new Widget(this, config as WidgetConfig); } - // ? - // this.editor.set(`skeleton.${widget.name}`, widget); return widget; } @@ -219,8 +227,6 @@ export class Skeleton { config = this.parseConfig(config); const panel = new Panel(this, config); this.panels.set(panel.name, panel); - // ? - // this.editor.set(`skeleton.${panel.name}`, panel); return panel; } diff --git a/packages/vision-polyfill/src/skeleton/transducers/addon-combine.ts b/packages/vision-polyfill/src/skeleton/transducers/addon-combine.ts new file mode 100644 index 000000000..80ad16cc1 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/transducers/addon-combine.ts @@ -0,0 +1,255 @@ +import { TransformedComponentMetadata, FieldConfig } from '@ali/lowcode-globals'; + +export default function(metadata: TransformedComponentMetadata): TransformedComponentMetadata { + const { componentName, configure = {} } = metadata; + if (componentName === 'Leaf') { + return { + ...metadata, + configure: { + ...configure, + combined: [ + { + name: 'children', + title: { type: 'i18n', 'zh-CN': '内容设置', 'en-US': 'Content' }, + setter: { + componentName: 'MixinSetter', + props: { + // TODO: + setters: [ + { + componentName: 'StringSetter', + props: { + // TODO: textarea mode + multiline: true, + }, + initialValue: '', + }, + { + componentName: 'ExpressionSetter', + initialValue: { + type: 'JSExpression', + value: '', + }, + }, + ], + }, + }, + }, + ], + }, + }; + } + + const { props, events = {}, styles } = configure as any; + const isRoot: boolean = componentName === 'Page' || componentName === 'Component'; + const eventsDefinition: any[] = []; + const supportedLifecycles = + events.supportedLifecycles || + (isRoot + ? [ + { + description: '初始化时', + name: 'constructor', + }, + { + description: '装载后', + name: 'componentDidMount', + }, + { + description: '更新时', + name: 'componentDidMount', + }, + { + description: '卸载时', + name: 'componentWillUnmount', + }, + ] + : null); + if (supportedLifecycles) { + eventsDefinition.push({ + type: 'lifeCycleEvent', + title: '生命周期', + list: supportedLifecycles.map((event: any) => (typeof event === 'string' ? { name: event } : event)), + }); + } + if (events.supportedEvents) { + eventsDefinition.push({ + type: 'events', + title: '事件', + list: (events.supportedEvents || []).map((event: any) => (typeof event === 'string' ? { name: event } : event)), + }); + } + // 通用设置 + const propsGroup = props || []; + propsGroup.push({ + name: '#generals', + title: { type: 'i18n', 'zh-CN': '通用', 'en-US': 'General' }, + items: [ + { + name: 'id', + title: 'ID', + setter: 'StringSetter', + }, + { + name: 'key', + title: 'Key', + // todo: use Mixin + setter: 'StringSetter', + }, + { + name: 'ref', + title: 'Ref', + setter: 'StringSetter', + }, + /* + { + name: '!more', + title: '更多', + setter: 'PropertiesSetter', + },*/ + ], + }); + const combined: FieldConfig[] = [ + { + title: { type: 'i18n', 'zh-CN': '属性', 'en-US': 'Props' }, + name: '#props', + items: propsGroup, + }, + ]; + const stylesGroup: FieldConfig[] = []; + if (styles?.supportClassName) { + stylesGroup.push({ + name: 'className', + title: { type: 'i18n', 'zh-CN': '类名绑定', 'en-US': 'ClassName' }, + setter: 'ClassNameSetter', + }); + } + if (styles?.supportInlineStyle) { + stylesGroup.push({ + name: 'style', + title: { type: 'i18n', 'zh-CN': '行内样式', 'en-US': 'Style' }, + setter: 'StyleSetter', + }); + } + if (stylesGroup.length > 0) { + combined.push({ + name: '#styles', + title: { type: 'i18n', 'zh-CN': '样式', 'en-US': 'Styles' }, + items: stylesGroup, + }); + } + + if (eventsDefinition.length > 0) { + combined.push({ + name: '#events', + title: { type: 'i18n', 'zh-CN': '事件', 'en-US': 'Events' }, + items: [ + { + name: '!events', + title: { type: 'i18n', 'zh-CN': '事件设置', 'en-US': 'Events' }, + setter: { + componentName: 'EventsSetter', + props: { + definition: eventsDefinition, + }, + }, + getValue(field: SettingField, val?: any[]) { + // todo: + return val; + }, + + setValue(field: SettingField, eventDataList: any[]) { + // todo: + return; + }, + }, + ], + }); + } + + if (isRoot) { + /* + combined.push({ + name: '#advanced', + title: { type: 'i18n', 'zh-CN': '高级', 'en-US': 'Advance' }, + items: [], + }); + */ + } else { + combined.push({ + name: '#advanced', + title: { type: 'i18n', 'zh-CN': '高级', 'en-US': 'Advance' }, + items: [ + { + name: '__condition', + title: { type: 'i18n', 'zh-CN': '条件显示', 'en-US': 'Condition' }, + setter: 'ExpressionSetter', + }, + { + name: '#loop', + title: { type: 'i18n', 'zh-CN': '循环', 'en-US': 'Loop' }, + items: [ + { + name: '__loop', + title: { type: 'i18n', 'zh-CN': '循环数据', 'en-US': 'Loop Data' }, + setter: { + componentName: 'MixinSetter', + props: { + // TODO: + setters: [ + { + componentName: 'JSONSetter', + props: { + mode: 'popup', + placeholder: { type: 'i18n', 'zh-CN': '编辑数据', 'en-US': 'Edit Data' }, + }, + }, + { + componentName: 'ExpressionSetter', + props: { + placeholder: { type: 'i18n', 'zh-CN': '绑定数据', 'en-US': 'Bind Data' }, + }, + }, + ], + }, + }, + }, + { + name: '__loopArgs.0', + title: { type: 'i18n', 'zh-CN': '迭代变量名', 'en-US': 'Loop Item' }, + setter: { + componentName: 'StringSetter', + props: { + placeholder: { type: 'i18n', 'zh-CN': '默认为: item', 'en-US': 'Defaults: item' }, + } + }, + }, + { + name: '__loopArgs.1', + title: { type: 'i18n', 'zh-CN': '索引变量名', 'en-US': 'Loop Index' }, + setter: { + componentName: 'StringSetter', + props: { + placeholder: { type: 'i18n', 'zh-CN': '默认为: index', 'en-US': 'Defaults: index' }, + } + }, + }, + { + name: 'key', + title: 'Key', + setter: 'ExpressionSetter', + }, + ], + }, + ], + }); + } + + return { + ...metadata, + configure: { + ...configure, + combined, + }, + }; +} diff --git a/packages/vision-polyfill/src/skeleton/transducers/parse-props.ts b/packages/vision-polyfill/src/skeleton/transducers/parse-props.ts new file mode 100644 index 000000000..43bd44221 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/transducers/parse-props.ts @@ -0,0 +1,232 @@ +import { + FieldConfig, + PropConfig, + PropType, + SetterType, + OneOf, + Shape, + ObjectOf, + ArrayOf, + TransformedComponentMetadata, +} from '@ali/lowcode-globals'; + +function propConfigToFieldConfig(propConfig: PropConfig): FieldConfig { + const { name, description } = propConfig; + const title = { + label: { + type: 'i18n', + 'en-US': name, + 'zh-CN': description?.slice(0, 10) || name, + }, + tip: description ? `${name} | ${description}` : undefined, + }; + return { + title, + ...propConfig, + setter: propTypeToSetter(propConfig.propType), + }; +} + +function propTypeToSetter(propType: PropType): SetterType { + let typeName: string; + let isRequired: boolean | undefined = false; + if (typeof propType === 'string') { + typeName = propType; + } else { + typeName = propType.type; + isRequired = propType.isRequired; + } + // TODO: use mixinSetter wrapper + switch (typeName) { + case 'string': + return { + componentName: 'StringSetter', + isRequired, + initialValue: '', + }; + + case 'number': + return { + componentName: 'NumberSetter', + isRequired, + initialValue: 0, + }; + case 'bool': + return { + componentName: 'NumberSetter', + isRequired, + initialValue: false, + }; + case 'oneOf': + const dataSource = ((propType as OneOf).value || []).map((value, index) => { + const t = typeof value; + return { + label: t === 'string' || t === 'number' || t === 'boolean' ? String(value) : `value ${index}`, + value, + }; + }); + const componentName = dataSource.length > 4 ? 'SelectSetter' : 'RadioGroupSetter'; + return { + componentName, + props: { dataSource }, + isRequired, + initialValue: dataSource[0] ? dataSource[0].value : null, + }; + + case 'element': + case 'node': // TODO: use Mixin + return { + // slotSetter + componentName: 'NodeSetter', + props: { + mode: typeName, + }, + isRequired, + initialValue: { + type: 'JSSlot', + value: '', + }, + }; + case 'shape': + case 'exact': + const items = (propType as Shape).value.map((item) => propConfigToFieldConfig(item)); + return { + componentName: 'ObjectSetter', + props: { + config: { + items, + extraSetter: typeName === 'shape' ? propTypeToSetter('any') : null, + }, + }, + isRequired, + initialValue: (field: any) => { + const data: any = {}; + items.forEach((item) => { + let initial = item.defaultValue; + if (initial == null && item.setter && typeof item.setter === 'object') { + initial = (item.setter as any).initialValue; + } + data[item.name] = initial ? (typeof initial === 'function' ? initial(field) : initial) : null; + }); + return data; + }, + }; + case 'object': + case 'objectOf': + return { + componentName: 'ObjectSetter', + props: { + config: { + extraSetter: propTypeToSetter(typeName === 'objectOf' ? (propType as ObjectOf).value : 'any'), + }, + }, + isRequired, + }; + case 'array': + case 'arrayOf': + return { + componentName: 'ArraySetter', + props: { + itemSetter: propTypeToSetter(typeName === 'arrayOf' ? (propType as ArrayOf).value : 'any'), + }, + isRequired, + initialValue: [], + }; + case 'func': + return { + componentName: 'FunctionSetter', + isRequired, + initialValue: { + type: 'JSFunction', + value: 'function(){}', + }, + }; + case 'oneOfType': + return { + componentName: 'MixinSetter', + props: { + // TODO: + // setters: (propType as OneOfType).value.map(item => propTypeToSetter(item)), + }, + isRequired, + }; + } + + return { + componentName: 'MixinSetter', + isRequired, + }; +} + +const EVENT_RE = /^on[A-Z][\w]*$/; + +export default function(metadata: TransformedComponentMetadata): TransformedComponentMetadata { + const { configure } = metadata; + if (configure.props) { + return metadata; + } + + if (!metadata.props) { + return { + ...metadata, + configure: { + ...configure, + props: [], + }, + }; + } + const { component = {}, events = {}, styles = {} } = configure; + const supportedEvents: any[] | null = (events as any).supportedEvents ? null : []; + const props: FieldConfig[] = []; + + metadata.props.forEach((prop) => { + const { name, propType, description } = prop; + if ( + name === 'children' && + (component.isContainer || propType === 'node' || propType === 'element' || propType === 'any') + ) { + if (component.isContainer !== false) { + component.isContainer = true; + return; + } + } + + if (EVENT_RE.test(name) && (propType === 'func' || propType === 'any')) { + if (supportedEvents) { + supportedEvents.push({ + name, + description, + }); + (events as any).supportedEvents = supportedEvents; + } + return; + } + + if (name === 'className' && (propType === 'string' || propType === 'any')) { + if ((styles as any).supportClassName == null) { + (styles as any).supportClassName = true; + } + return; + } + + if (name === 'style' && (propType === 'object' || propType === 'any')) { + if ((styles as any).supportInlineStyle == null) { + (styles as any).supportInlineStyle = true; + } + return; + } + + props.push(propConfigToFieldConfig(prop)); + }); + + return { + ...metadata, + configure: { + ...configure, + props, + events, + styles, + component, + }, + }; +} diff --git a/packages/vision-polyfill/src/skeleton/transducers/register.ts b/packages/vision-polyfill/src/skeleton/transducers/register.ts new file mode 100644 index 000000000..a09e685ed --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/transducers/register.ts @@ -0,0 +1,9 @@ +import { registerMetadataTransducer } from '@ali/lowcode-globals'; +import parseProps from './parse-props'; +import addonCombine from './addon-combine'; + +// parseProps +registerMetadataTransducer(parseProps, 10, 'parse-props'); + +// addon/platform custom +registerMetadataTransducer(addonCombine, 11, 'combine-props'); diff --git a/packages/vision-polyfill/src/skeleton/types.ts b/packages/vision-polyfill/src/skeleton/types.ts index 2ae7df543..b44b7b165 100644 --- a/packages/vision-polyfill/src/skeleton/types.ts +++ b/packages/vision-polyfill/src/skeleton/types.ts @@ -14,7 +14,6 @@ export interface IWidgetBaseConfig { export interface WidgetConfig extends IWidgetBaseConfig { type: "Widget"; - name: string; // as pluginKey props?: { align?: "left" | "right" | "bottom" | "center" | "top"; }; @@ -22,7 +21,7 @@ export interface WidgetConfig extends IWidgetBaseConfig { } export function isWidgetConfig(obj: any): obj is WidgetConfig { - return obj && obj.type === "Custom"; + return obj && obj.type === "Widget"; } export interface DockProps { @@ -34,6 +33,17 @@ export interface DockProps { onClick?: () => void; } +export interface DividerConfig extends IWidgetBaseConfig { + type: "Divider"; + props?: { + align?: "left" | "right" | "center"; + }; +} + +export function isDividerConfig(obj: any): obj is DividerConfig { + return obj && obj.type === "Divider"; +} + export interface IDockBaseConfig extends IWidgetBaseConfig { props?: DockProps & { align?: "left" | "right" | "bottom" | "center" | "top"; diff --git a/packages/vision-polyfill/src/skeleton/widget/dialog-dock.ts b/packages/vision-polyfill/src/skeleton/widget/dialog-dock.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/vision-polyfill/src/skeleton/widget/dock.ts b/packages/vision-polyfill/src/skeleton/widget/dock.ts new file mode 100644 index 000000000..019b2e321 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget/dock.ts @@ -0,0 +1,86 @@ +import { ReactNode, createElement } from 'react'; +import { uniqueId, createContent, obx } from '@ali/lowcode-globals'; +import { DockConfig } from "../types"; +import { Skeleton } from '../skeleton'; +import { DockView, WidgetView } from '../components/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; + } + + get content(): ReactNode { + return createElement(WidgetView, { + widget: this, + key: this.id, + }); + } + + private inited: boolean = false; + private _body: ReactNode; + get body() { + if (this.inited) { + return this._body; + } + + const { props, content, contentProps } = this.config; + + if (content) { + this._body = createContent(content, { + ...contentProps, + config: this.config, + editor: this.skeleton.editor, + }); + } else { + this._body = createElement(DockView, props); + } + return this._body; + } + + constructor(readonly skeleton: Skeleton, readonly 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); + } + + toggle() { + this.setVisible(!this._visible); + } +} diff --git a/packages/vision-polyfill/src/skeleton/panel-dock.ts b/packages/vision-polyfill/src/skeleton/widget/panel-dock.ts similarity index 92% rename from packages/vision-polyfill/src/skeleton/panel-dock.ts rename to packages/vision-polyfill/src/skeleton/widget/panel-dock.ts index 7deca60c9..c47d3290a 100644 --- a/packages/vision-polyfill/src/skeleton/panel-dock.ts +++ b/packages/vision-polyfill/src/skeleton/widget/panel-dock.ts @@ -1,11 +1,10 @@ import { uniqueId, obx, computed } from '@ali/lowcode-globals'; import { createElement, ReactNode } from 'react'; -import { Skeleton } from './skeleton'; -import { PanelDockConfig } from './types'; +import { Skeleton } from '../skeleton'; +import { PanelDockConfig } from '../types'; import Panel from './panel'; -import { PanelDockView, WidgetView } from './widget-views'; +import { PanelDockView, WidgetView } from '../components/widget-views'; import { IWidget } from './widget'; -import { composeTitle } from './utils'; export default class PanelDock implements IWidget { readonly isWidget = true; diff --git a/packages/vision-polyfill/src/skeleton/widget/panel.ts b/packages/vision-polyfill/src/skeleton/widget/panel.ts new file mode 100644 index 000000000..9a33c3e44 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget/panel.ts @@ -0,0 +1,164 @@ +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 { TitledPanelView, TabsPanelView, PanelView } from '../components/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; + } + + readonly isPanel = true; + + private _body?: ReactNode; + get body() { + this.initBody(); + return this._body; + } + + get content(): ReactNode { + if (this.plain) { + return createElement(PanelView, { + panel: this, + key: this.id, + }); + } + return createElement(TitledPanelView, { panel: this, key: this.id }); + } + + readonly title: TitleContent; + readonly help?: HelpTipConfig; + private plain: boolean = false; + + private container?: WidgetContainer<Panel, PanelConfig>; + private parent?: WidgetContainer; + + constructor(readonly skeleton: Skeleton, readonly 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, + }); + } else { + const { content, contentProps } = this.config; + this._body = createContent(content, { + ...contentProps, + editor: this.skeleton.editor, + config: this.config, + panel: this, + }); + } + } + + 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; + } + + 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); + } + + 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/widget/stage.ts b/packages/vision-polyfill/src/skeleton/widget/stage.ts new file mode 100644 index 000000000..a4bf483f2 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget/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/widget/utils.ts b/packages/vision-polyfill/src/skeleton/widget/utils.ts new file mode 100644 index 000000000..301426a87 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget/utils.ts @@ -0,0 +1,28 @@ +import { IconType, TitleContent, isI18nData, TipContent } from '@ali/lowcode-globals'; +import { isValidElement } from 'react'; + +export function composeTitle(title?: TitleContent, icon?: IconType, tip?: TipContent, tipAsTitle?: boolean) { + if (!title) { + title = {}; + if (!icon || tipAsTitle) { + 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/widget-container.ts b/packages/vision-polyfill/src/skeleton/widget/widget-container.ts new file mode 100644 index 000000000..b34827ae7 --- /dev/null +++ b/packages/vision-polyfill/src/skeleton/widget/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<T extends WidgetItem = any, G extends WidgetItem = any> { + @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.ts b/packages/vision-polyfill/src/skeleton/widget/widget.ts similarity index 92% rename from packages/vision-polyfill/src/skeleton/widget.ts rename to packages/vision-polyfill/src/skeleton/widget/widget.ts index 3b04672b8..1f7a7d84a 100644 --- a/packages/vision-polyfill/src/skeleton/widget.ts +++ b/packages/vision-polyfill/src/skeleton/widget/widget.ts @@ -1,8 +1,8 @@ import { ReactNode, createElement } from 'react'; import { createContent, uniqueId, obx } from '@ali/lowcode-globals'; -import { WidgetConfig, IWidgetBaseConfig } from './types'; -import { Skeleton } from './skeleton'; -import { WidgetView } from './widget-views'; +import { WidgetConfig, IWidgetBaseConfig } from '../types'; +import { Skeleton } from '../skeleton'; +import { WidgetView } from '../components/widget-views'; export interface IWidget { readonly name: string; diff --git a/packages/vision-polyfill/src/vision.ts b/packages/vision-polyfill/src/vision.ts index c8c079467..7bca93882 100644 --- a/packages/vision-polyfill/src/vision.ts +++ b/packages/vision-polyfill/src/vision.ts @@ -9,7 +9,7 @@ import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS } from './const'; import Bus from './bus'; import Symbols from './symbols'; import { skeleton, editor } from './editor'; -import { VisionWorkbench } from './skeleton/workbench'; +import { VisionWorkbench } from './skeleton/layouts/workbench'; import Panes from './panes'; import Exchange from './exchange'; import VisualEngineContext from './context';