From dc8fdae09d119f448dddd784433d04c0fd088166 Mon Sep 17 00:00:00 2001 From: kangwei Date: Mon, 23 Mar 2020 01:14:12 +0800 Subject: [PATCH] prepare outline --- packages/designer/README.md | 3 + packages/designer/package.json | 2 +- .../src/builtins/drag-ghost/index.tsx | 3 +- .../builtins/simulator/host/auxilary/index.ts | 1 - .../bem-tools.less} | 2 +- .../border-hovering.tsx} | 17 +- .../border-selecting.tsx} | 33 +- .../outlines.less => bem-tools/borders.less} | 2 +- .../auxiliary.tsx => bem-tools/index.tsx} | 18 +- .../{auxilary => bem-tools}/insertion.less | 0 .../{auxilary => bem-tools}/insertion.tsx | 3 +- .../src/builtins/simulator/host/host-view.tsx | 6 +- .../src/builtins/simulator/host/host.ts | 44 +- .../simulator/host/resource-consumer.ts | 9 +- .../src/builtins/simulator/host/viewport.ts | 2 +- .../builtins/simulator/renderer/renderer.ts | 2 +- .../builtins/simulator/utils/parse-props.ts | 2 +- .../designer/src/designer/component-meta.ts | 166 ++----- packages/designer/src/designer/designer.ts | 26 +- .../src/designer/document/document-model.ts | 14 +- .../src/designer/document/document-view.tsx | 2 +- .../designer/document/node/node-children.ts | 11 +- .../src/designer/document/node/node.ts | 39 +- .../document/node/props/prop-stash.ts | 2 +- .../src/designer/document/node/props/prop.ts | 15 +- .../src/designer/document/node/props/props.ts | 9 +- .../src/designer/document/node/root-node.ts | 3 +- .../src/designer/document/selection.ts | 2 +- .../designer/src/designer/helper/dragon.ts | 3 +- .../designer/src/designer/helper/history.ts | 49 +- .../designer/src/designer/helper/hovering.ts | 6 +- .../src/designer/helper/offset-observer.ts | 2 +- .../designer/src/designer/helper/session.ts | 44 -- .../designer/src/designer/project-view.tsx | 2 +- packages/designer/src/designer/project.ts | 18 +- packages/designer/src/designer/schema.ts | 158 ------ packages/designer/src/designer/simulator.ts | 4 +- packages/editor/src/index.tsx | 2 +- packages/globals/README.md | 2 + packages/globals/package.json | 2 + .../globals/src/components/tip/embed-tip.tsx | 10 +- .../globals/src/components/tip/tip-handler.ts | 2 +- packages/globals/src/components/tip/tip.tsx | 7 +- .../globals/src/components/title/index.tsx | 42 +- .../globals/src/components/title/title.less | 3 +- packages/globals/src/di/index.ts | 2 + packages/globals/src/di/setter.ts | 43 ++ packages/globals/src/di/transducer.ts | 12 + packages/globals/src/icons/clone.tsx | 4 +- packages/globals/src/icons/component.tsx | 10 + packages/globals/src/icons/container.tsx | 11 + packages/globals/src/icons/hidden.tsx | 4 +- packages/globals/src/icons/index.ts | 5 +- packages/globals/src/icons/page.tsx | 12 + packages/globals/src/icons/remove.tsx | 4 +- .../src/icons/{settings.tsx => setting.tsx} | 4 +- packages/globals/src/index.ts | 3 + .../globals/src/intl/ali-global-locale.ts | 4 + packages/globals/src/intl/index.tsx | 56 ++- packages/globals/src/obx/index.ts | 4 + packages/globals/src/types/data-source.ts | 16 + packages/globals/src/types/field-config.ts | 58 +++ packages/globals/src/types/i18n.ts | 13 + packages/globals/src/types/icon.ts | 9 + packages/globals/src/types/index.ts | 13 + packages/globals/src/types/metadata.ts | 87 ++++ packages/globals/src/types/npm.ts | 11 + .../src/types}/prop-config.ts | 0 packages/globals/src/types/schema.ts | 80 +++ packages/globals/src/types/setter-config.ts | 34 ++ packages/globals/src/types/tip.ts | 11 + packages/globals/src/types/title.ts | 15 + packages/globals/src/types/utils.ts | 13 + packages/globals/src/types/value-type.ts | 42 ++ packages/globals/src/utils/create-icon.tsx | 22 +- packages/plugin-outline-pane/.eslintignore | 6 + packages/plugin-outline-pane/.eslintrc | 3 + packages/plugin-outline-pane/.prettierrc | 6 + packages/plugin-outline-pane/package.json | 43 ++ packages/plugin-outline-pane/src/README.md | 1 + .../src/helper/dwell-timer.ts | 37 ++ .../src/helper/is-container.ts | 13 + .../src/helper/x-axis-tracker.ts | 111 +++++ .../src/icons/border-outer.svg | 1 + .../src/icons/caret-down.svg | 1 + .../src/icons/caret-right.svg | 1 + packages/plugin-outline-pane/src/index.ts | 10 + .../plugin-outline-pane/src/locale/en-US.json | 4 + .../plugin-outline-pane/src/locale/index.ts | 10 + .../plugin-outline-pane/src/locale/zh-CN.json | 4 + packages/plugin-outline-pane/src/main.ts | 101 ++++ packages/plugin-outline-pane/src/sensor.ts | 220 ++++++++ packages/plugin-outline-pane/src/tree-node.ts | 233 +++++++++ packages/plugin-outline-pane/src/tree.ts | 23 + .../plugin-outline-pane/src/views/pane.less | 251 ++++++++++ .../plugin-outline-pane/src/views/pane.tsx | 47 ++ .../src/views/tree-branches.tsx | 56 +++ .../src/views/tree-node-icon-view.tsx | 23 + .../src/views/tree-node.tsx | 58 +++ .../src/views/tree-title.tsx | 176 +++++++ .../plugin-outline-pane/src/views/tree.tsx | 81 +++ packages/plugin-outline-pane/tsconfig.json | 9 + packages/plugin-setters/.eslintignore | 6 + packages/plugin-setters/.eslintrc | 3 + packages/plugin-setters/.prettierrc | 6 + packages/plugin-setters/package.json | 40 +- .../src/index.tsx} | 35 +- packages/plugin-setters/tsconfig.json | 9 + packages/plugin-settings/src/index.tsx | 28 +- packages/plugin-settings/src/main.ts | 97 +--- .../src/register-transducer.ts | 469 ------------------ .../array-setter/index.tsx | 0 .../array-setter/sortable.less | 0 .../array-setter/sortable.tsx | 0 .../array-setter/style.less | 0 .../object-setter/index.tsx | 0 .../object-setter/style.less | 0 .../plugin-settings/src/setters/register.ts | 6 + .../plugin-settings/src/settings-pane.tsx | 60 +-- packages/plugin-settings/src/style.less | 1 + .../src/transducers/addon-combine.ts | 253 ++++++++++ .../src/transducers/parse-props.ts | 222 +++++++++ .../src/transducers/register.ts | 9 + packages/protocal/README.md | 1 - 124 files changed, 3005 insertions(+), 1165 deletions(-) delete mode 100644 packages/designer/src/builtins/simulator/host/auxilary/index.ts rename packages/designer/src/builtins/simulator/host/{auxilary/auxiliary.less => bem-tools/bem-tools.less} (89%) rename packages/designer/src/builtins/simulator/host/{auxilary/outline-hovering.tsx => bem-tools/border-hovering.tsx} (85%) rename packages/designer/src/builtins/simulator/host/{auxilary/outline-selecting.tsx => bem-tools/border-selecting.tsx} (84%) rename packages/designer/src/builtins/simulator/host/{auxilary/outlines.less => bem-tools/borders.less} (98%) rename packages/designer/src/builtins/simulator/host/{auxilary/auxiliary.tsx => bem-tools/index.tsx} (56%) rename packages/designer/src/builtins/simulator/host/{auxilary => bem-tools}/insertion.less (100%) rename packages/designer/src/builtins/simulator/host/{auxilary => bem-tools}/insertion.tsx (97%) delete mode 100644 packages/designer/src/designer/helper/session.ts delete mode 100644 packages/designer/src/designer/schema.ts create mode 100644 packages/globals/src/di/index.ts create mode 100644 packages/globals/src/di/setter.ts create mode 100644 packages/globals/src/di/transducer.ts create mode 100644 packages/globals/src/icons/component.tsx create mode 100644 packages/globals/src/icons/container.tsx create mode 100644 packages/globals/src/icons/page.tsx rename packages/globals/src/icons/{settings.tsx => setting.tsx} (96%) create mode 100644 packages/globals/src/obx/index.ts create mode 100644 packages/globals/src/types/data-source.ts create mode 100644 packages/globals/src/types/field-config.ts create mode 100644 packages/globals/src/types/i18n.ts create mode 100644 packages/globals/src/types/icon.ts create mode 100644 packages/globals/src/types/index.ts create mode 100644 packages/globals/src/types/metadata.ts create mode 100644 packages/globals/src/types/npm.ts rename packages/{designer/src/designer => globals/src/types}/prop-config.ts (100%) create mode 100644 packages/globals/src/types/schema.ts create mode 100644 packages/globals/src/types/setter-config.ts create mode 100644 packages/globals/src/types/tip.ts create mode 100644 packages/globals/src/types/title.ts create mode 100644 packages/globals/src/types/utils.ts create mode 100644 packages/globals/src/types/value-type.ts create mode 100644 packages/plugin-outline-pane/.eslintignore create mode 100644 packages/plugin-outline-pane/.eslintrc create mode 100644 packages/plugin-outline-pane/.prettierrc create mode 100644 packages/plugin-outline-pane/package.json create mode 100644 packages/plugin-outline-pane/src/README.md create mode 100644 packages/plugin-outline-pane/src/helper/dwell-timer.ts create mode 100644 packages/plugin-outline-pane/src/helper/is-container.ts create mode 100644 packages/plugin-outline-pane/src/helper/x-axis-tracker.ts create mode 100644 packages/plugin-outline-pane/src/icons/border-outer.svg create mode 100644 packages/plugin-outline-pane/src/icons/caret-down.svg create mode 100644 packages/plugin-outline-pane/src/icons/caret-right.svg create mode 100644 packages/plugin-outline-pane/src/index.ts create mode 100644 packages/plugin-outline-pane/src/locale/en-US.json create mode 100644 packages/plugin-outline-pane/src/locale/index.ts create mode 100644 packages/plugin-outline-pane/src/locale/zh-CN.json create mode 100644 packages/plugin-outline-pane/src/main.ts create mode 100644 packages/plugin-outline-pane/src/sensor.ts create mode 100644 packages/plugin-outline-pane/src/tree-node.ts create mode 100644 packages/plugin-outline-pane/src/tree.ts create mode 100644 packages/plugin-outline-pane/src/views/pane.less create mode 100644 packages/plugin-outline-pane/src/views/pane.tsx create mode 100644 packages/plugin-outline-pane/src/views/tree-branches.tsx create mode 100644 packages/plugin-outline-pane/src/views/tree-node-icon-view.tsx create mode 100644 packages/plugin-outline-pane/src/views/tree-node.tsx create mode 100644 packages/plugin-outline-pane/src/views/tree-title.tsx create mode 100644 packages/plugin-outline-pane/src/views/tree.tsx create mode 100644 packages/plugin-outline-pane/tsconfig.json create mode 100644 packages/plugin-setters/.eslintignore create mode 100644 packages/plugin-setters/.eslintrc create mode 100644 packages/plugin-setters/.prettierrc rename packages/{editor/src/config/setters.ts => plugin-setters/src/index.tsx} (54%) create mode 100644 packages/plugin-setters/tsconfig.json delete mode 100644 packages/plugin-settings/src/register-transducer.ts rename packages/plugin-settings/src/{builtin-setters => setters}/array-setter/index.tsx (100%) rename packages/plugin-settings/src/{builtin-setters => setters}/array-setter/sortable.less (100%) rename packages/plugin-settings/src/{builtin-setters => setters}/array-setter/sortable.tsx (100%) rename packages/plugin-settings/src/{builtin-setters => setters}/array-setter/style.less (100%) rename packages/plugin-settings/src/{builtin-setters => setters}/object-setter/index.tsx (100%) rename packages/plugin-settings/src/{builtin-setters => setters}/object-setter/style.less (100%) create mode 100644 packages/plugin-settings/src/setters/register.ts create mode 100644 packages/plugin-settings/src/transducers/addon-combine.ts create mode 100644 packages/plugin-settings/src/transducers/parse-props.ts create mode 100644 packages/plugin-settings/src/transducers/register.ts delete mode 100644 packages/protocal/README.md diff --git a/packages/designer/README.md b/packages/designer/README.md index 17e23305b..ce7ea5d04 100644 --- a/packages/designer/README.md +++ b/packages/designer/README.md @@ -1 +1,4 @@ 编排模块 + + +simulator/renderer 发 CDN diff --git a/packages/designer/package.json b/packages/designer/package.json index 95d06f83c..ec3441d5a 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -1,5 +1,5 @@ { - "name": "lowcode-designer", + "name": "@ali/lowcode-designer", "version": "0.9.0", "description": "alibaba lowcode designer", "main": "src/index.ts", diff --git a/packages/designer/src/builtins/drag-ghost/index.tsx b/packages/designer/src/builtins/drag-ghost/index.tsx index 29922ebea..29d65c752 100644 --- a/packages/designer/src/builtins/drag-ghost/index.tsx +++ b/packages/designer/src/builtins/drag-ghost/index.tsx @@ -1,6 +1,5 @@ import { Component } from 'react'; -import { obx } from '@recore/obx'; -import { observer } from '@recore/obx-react'; +import { observer, obx } from '../../../../globals'; import Designer from '../../designer/designer'; import { isDragNodeObject, DragObject, isDragNodeDataObject } from '../../designer/helper/dragon'; import './ghost.less'; diff --git a/packages/designer/src/builtins/simulator/host/auxilary/index.ts b/packages/designer/src/builtins/simulator/host/auxilary/index.ts deleted file mode 100644 index 61552f4e3..000000000 --- a/packages/designer/src/builtins/simulator/host/auxilary/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auxiliary'; diff --git a/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.less b/packages/designer/src/builtins/simulator/host/bem-tools/bem-tools.less similarity index 89% rename from packages/designer/src/builtins/simulator/host/auxilary/auxiliary.less rename to packages/designer/src/builtins/simulator/host/bem-tools/bem-tools.less index af0b195bc..83a411e56 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.less +++ b/packages/designer/src/builtins/simulator/host/bem-tools/bem-tools.less @@ -1,4 +1,4 @@ -.lc-auxiliary { +.lc-bem-tools { pointer-events: none; position: absolute; top: 0; diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx b/packages/designer/src/builtins/simulator/host/bem-tools/border-hovering.tsx similarity index 85% rename from packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx rename to packages/designer/src/builtins/simulator/host/bem-tools/border-hovering.tsx index 412490df4..3336a3598 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx +++ b/packages/designer/src/builtins/simulator/host/bem-tools/border-hovering.tsx @@ -1,12 +1,11 @@ import { Component, Fragment, PureComponent } from 'react'; import classNames from 'classnames'; -import { observer } from '@recore/obx-react'; import { SimulatorContext } from '../context'; import { SimulatorHost } from '../host'; -import { computed } from '@recore/obx'; +import { computed, observer, TitleContent, Title } from '../../../../../../globals/src'; -export class OutlineHoveringInstance extends PureComponent<{ - title: string; +export class BorderHoveringInstance extends PureComponent<{ + title: TitleContent; rect: DOMRect | null; scale: number; scrollX: number; @@ -24,7 +23,7 @@ export class OutlineHoveringInstance extends PureComponent<{ transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`, }; - const className = classNames('lc-outlines lc-outlines-hovering'); + const className = classNames('lc-borders lc-borders-hovering'); // TODO: // 1. thinkof icon @@ -32,14 +31,14 @@ export class OutlineHoveringInstance extends PureComponent<{ return (
- {title} + </div> ); } } @observer -export class OutlineHovering extends Component { +export class BorderHovering extends Component { static contextType = SimulatorContext; shouldComponentUpdate() { @@ -82,7 +81,7 @@ export class OutlineHovering extends Component { if (instances.length === 1) { return ( - <OutlineHoveringInstance + <BorderHoveringInstance key="line-h" title={current.title} scale={this.scale} @@ -95,7 +94,7 @@ export class OutlineHovering extends Component { return ( <Fragment> {instances.map((inst, i) => ( - <OutlineHoveringInstance + <BorderHoveringInstance key={`line-h-${i}`} title={current.title} scale={this.scale} diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx b/packages/designer/src/builtins/simulator/host/bem-tools/border-selecting.tsx similarity index 84% rename from packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx rename to packages/designer/src/builtins/simulator/host/bem-tools/border-selecting.tsx index 4b7cf86fb..3f4f453fa 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx +++ b/packages/designer/src/builtins/simulator/host/bem-tools/border-selecting.tsx @@ -9,17 +9,22 @@ import { ComponentType, } from 'react'; import classNames from 'classnames'; -import { observer } from '@recore/obx-react'; import { SimulatorContext } from '../context'; import { SimulatorHost } from '../host'; -import { computed } from '@recore/obx'; import OffsetObserver from '../../../../designer/helper/offset-observer'; import Node from '../../../../designer/document/node/node'; -import { isContentObject, ContentObject } from '../../../../designer/component-meta'; -import { createIcon, EmbedTip, isReactComponent } from '../../../../../../globals'; +import { + observer, + computed, + createIcon, + EmbedTip, + isReactComponent, + ActionContentObject, + isActionContentObject, +} from '../../../../../../globals'; @observer -export class OutlineSelectingInstance extends Component<{ +export class BorderSelectingInstance extends Component<{ observed: OffsetObserver; highlight?: boolean; dragging?: boolean; @@ -42,7 +47,7 @@ export class OutlineSelectingInstance extends Component<{ transform: `translate3d(${offsetLeft}px, ${offsetTop}px, 0)`, }; - const className = classNames('lc-outlines lc-outlines-selecting', { + const className = classNames('lc-borders lc-borders-selecting', { highlight, dragging, }); @@ -100,26 +105,26 @@ class Toolbar extends Component<{ observed: OffsetObserver }> { } }); return ( - <div className="lc-outlines-actions" style={style}> + <div className="lc-borders-actions" style={style}> {actions} </div> ); } } -function createAction(content: ReactNode | ComponentType<any> | ContentObject, key: string, node: Node) { +function createAction(content: ReactNode | ComponentType<any> | ActionContentObject, key: string, node: Node) { if (isValidElement(content)) { return cloneElement(content, { key, node }); } if (isReactComponent(content)) { return createElement(content, { key, node }); } - if (isContentObject(content)) { + if (isActionContentObject(content)) { const { action, description, icon } = content; return ( <div key={key} - className="lc-outlines-action" + className="lc-borders-action" onClick={() => { action && action(node); }} @@ -133,7 +138,7 @@ function createAction(content: ReactNode | ComponentType<any> | ContentObject, k } @observer -export class OutlineSelectingForNode extends Component<{ node: Node }> { +export class BorderSelectingForNode extends Component<{ node: Node }> { static contextType = SimulatorContext; get host(): SimulatorHost { @@ -170,7 +175,7 @@ export class OutlineSelectingForNode extends Component<{ node: Node }> { if (!observed) { return null; } - return <OutlineSelectingInstance key={observed.id} dragging={this.dragging} observed={observed} />; + return <BorderSelectingInstance key={observed.id} dragging={this.dragging} observed={observed} />; })} </Fragment> ); @@ -178,7 +183,7 @@ export class OutlineSelectingForNode extends Component<{ node: Node }> { } @observer -export class OutlineSelecting extends Component { +export class BorderSelecting extends Component { static contextType = SimulatorContext; get host(): SimulatorHost { @@ -212,7 +217,7 @@ export class OutlineSelecting extends Component { return ( <Fragment> {selecting.map(node => ( - <OutlineSelectingForNode key={node.id} node={node} /> + <BorderSelectingForNode key={node.id} node={node} /> ))} </Fragment> ); diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outlines.less b/packages/designer/src/builtins/simulator/host/bem-tools/borders.less similarity index 98% rename from packages/designer/src/builtins/simulator/host/auxilary/outlines.less rename to packages/designer/src/builtins/simulator/host/bem-tools/borders.less index 19f4333ec..511bae9c2 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/outlines.less +++ b/packages/designer/src/builtins/simulator/host/bem-tools/borders.less @@ -1,4 +1,4 @@ -@scope: lc-outlines; +@scope: lc-borders; .@{scope} { box-sizing: border-box; diff --git a/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.tsx b/packages/designer/src/builtins/simulator/host/bem-tools/index.tsx similarity index 56% rename from packages/designer/src/builtins/simulator/host/auxilary/auxiliary.tsx rename to packages/designer/src/builtins/simulator/host/bem-tools/index.tsx index 25aa33bbf..457fbdc3f 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.tsx +++ b/packages/designer/src/builtins/simulator/host/bem-tools/index.tsx @@ -1,15 +1,15 @@ -import { observer } from '@recore/obx-react'; import { Component } from 'react'; -import { OutlineHovering } from './outline-hovering'; +import { observer } from '../../../../../../globals'; +import { BorderHovering } from './border-hovering'; import { SimulatorContext } from '../context'; import { SimulatorHost } from '../host'; -import { OutlineSelecting } from './outline-selecting'; +import { BorderSelecting } from './border-selecting'; import { InsertionView } from './insertion'; -import './auxiliary.less'; -import './outlines.less'; +import './bem-tools.less'; +import './borders.less'; @observer -export class AuxiliaryView extends Component { +export class BemTools extends Component { static contextType = SimulatorContext; shouldComponentUpdate() { @@ -20,9 +20,9 @@ export class AuxiliaryView extends Component { const host = this.context as SimulatorHost; const { scrollX, scrollY, scale } = host.viewport; return ( - <div className="lc-auxiliary" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}> - <OutlineHovering key="hovering" /> - <OutlineSelecting key="selecting" /> + <div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}> + <BorderHovering key="hovering" /> + <BorderSelecting key="selecting" /> <InsertionView key="insertion" /> </div> ); diff --git a/packages/designer/src/builtins/simulator/host/auxilary/insertion.less b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.less similarity index 100% rename from packages/designer/src/builtins/simulator/host/auxilary/insertion.less rename to packages/designer/src/builtins/simulator/host/bem-tools/insertion.less diff --git a/packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx similarity index 97% rename from packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx rename to packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx index a61d2fc74..f36993253 100644 --- a/packages/designer/src/builtins/simulator/host/auxilary/insertion.tsx +++ b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx @@ -1,6 +1,5 @@ import { Component } from 'react'; -import { computed } from '@recore/obx'; -import { observer } from '@recore/obx-react'; +import { computed, observer } from '../../../../../../globals'; import { SimulatorContext } from '../context'; import { SimulatorHost } from '../host'; import Location, { diff --git a/packages/designer/src/builtins/simulator/host/host-view.tsx b/packages/designer/src/builtins/simulator/host/host-view.tsx index 0ab951d69..22e4e8044 100644 --- a/packages/designer/src/builtins/simulator/host/host-view.tsx +++ b/packages/designer/src/builtins/simulator/host/host-view.tsx @@ -1,9 +1,9 @@ import { Component } from 'react'; -import { observer } from '@recore/obx-react'; +import { observer } from '../../../../../globals'; import { SimulatorHost, SimulatorProps } from './host'; import DocumentModel from '../../../designer/document/document-model'; import { SimulatorContext } from './context'; -import { AuxiliaryView } from './auxilary'; +import { BemTools } from './bem-tools'; import './host.less'; /* @@ -65,7 +65,7 @@ class Canvas extends Component { return ( <div className={className}> <div ref={elmt => sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport"> - <AuxiliaryView /> + <BemTools /> <Content /> </div> </div> diff --git a/packages/designer/src/builtins/simulator/host/host.ts b/packages/designer/src/builtins/simulator/host/host.ts index 2b81ede54..d8feb15ea 100644 --- a/packages/designer/src/builtins/simulator/host/host.ts +++ b/packages/designer/src/builtins/simulator/host/host.ts @@ -1,4 +1,4 @@ -import { obx, autorun, computed } from '@recore/obx'; +import { obx, autorun, computed } from '../../../../../globals'; import { ISimulator, Component, NodeInstance } from '../../../designer/simulator'; import Viewport from './viewport'; import { createSimulator } from './create-simulator'; @@ -28,12 +28,11 @@ import { Rect, CanvasPoint, } from '../../../designer/helper/location'; -import { isNodeSchema, NodeSchema } from '../../../designer/schema'; -import { ComponentMetadata } from '../../../designer/component-meta'; import { ReactInstance } from 'react'; import { isRootNode } from '../../../designer/document/node/root-node'; import { parseProps } from '../utils/parse-props'; import { isElement } from '../../../utils/is-element'; +import { ComponentMetadata, NodeSchema, isNodeSchema } from '../../../../../globals'; export interface LibraryItem { package: string; @@ -464,24 +463,17 @@ export class SimulatorHost implements ISimulator<SimulatorProps> { return null; } + let elems = elements.slice(); + if (selector) { + const matched = getMatched(elems, selector); + if (!matched) { + return null; + } + elems = [matched]; + } let rects: DOMRect[] | undefined; let last: { x: number; y: number; r: number; b: number } | undefined; let computed = false; - const elems = selector - ? elements - .map(elem => { - if (isElement(elem)) { - // TODO: if has selector use exact match - if (elem.matches(selector)) { - return elem; - } - - return elem.querySelector(selector); - } - return null; - }) - .filter(Boolean) - : elements.slice(); while (true) { if (!rects || rects.length < 1) { const elem = elems.pop(); @@ -1057,3 +1049,19 @@ function isNearAfter(point: CanvasPoint, rect: Rect, inline: boolean) { } return Math.abs(point.canvasY - rect.top) > Math.abs(point.canvasY - rect.bottom); } + +function getMatched(elements: Array<Element | Text>, selector: string): Element | null { + let firstQueried: Element | null = null; + for (const elem of elements) { + if (isElement(elem)) { + if (elem.matches(selector)) { + return elem; + } + + if (!firstQueried) { + firstQueried = elem.querySelector(selector); + } + } + } + return firstQueried; +} diff --git a/packages/designer/src/builtins/simulator/host/resource-consumer.ts b/packages/designer/src/builtins/simulator/host/resource-consumer.ts index b68384812..8731e1afe 100644 --- a/packages/designer/src/builtins/simulator/host/resource-consumer.ts +++ b/packages/designer/src/builtins/simulator/host/resource-consumer.ts @@ -1,5 +1,5 @@ import { SimulatorRenderer } from '../renderer/renderer'; -import { autorun, obx } from '@recore/obx'; +import { autorun, obx } from '../../../../../globals'; import { SimulatorHost } from './host'; import { EventEmitter } from 'events'; @@ -34,7 +34,6 @@ export default class ResourceConsumer<T = any> { }); } - private _consuming?: () => void; consume(consumerOrRenderer: SimulatorRenderer | ((data: T) => any)) { if (this._consuming) { @@ -48,7 +47,7 @@ export default class ResourceConsumer<T = any> { } const rendererConsumer = this.consumer!; - consumer = (data) => rendererConsumer(consumerOrRenderer, data); + consumer = data => rendererConsumer(consumerOrRenderer, data); } else { consumer = consumerOrRenderer; } @@ -76,14 +75,14 @@ export default class ResourceConsumer<T = any> { this.emitter.removeAllListeners(); } - private _firstConsumed: boolean = false; + private _firstConsumed = false; private resovleFirst?: () => void; waitFirstConsume(): Promise<any> { if (this._firstConsumed) { return Promise.resolve(); } - return new Promise((resolve) => { + return new Promise(resolve => { this.resovleFirst = resolve; }); } diff --git a/packages/designer/src/builtins/simulator/host/viewport.ts b/packages/designer/src/builtins/simulator/host/viewport.ts index a22f87704..39208be2b 100644 --- a/packages/designer/src/builtins/simulator/host/viewport.ts +++ b/packages/designer/src/builtins/simulator/host/viewport.ts @@ -1,4 +1,4 @@ -import { obx, computed } from '@recore/obx'; +import { obx, computed } from '../../../../../globals'; import { Point } from '../../../designer/helper/location'; import { ScrollTarget } from '../../../designer/helper/scroller'; import { AutoFit, IViewport } from '../../../designer/simulator'; diff --git a/packages/designer/src/builtins/simulator/renderer/renderer.ts b/packages/designer/src/builtins/simulator/renderer/renderer.ts index 26c246399..818825bf2 100644 --- a/packages/designer/src/builtins/simulator/renderer/renderer.ts +++ b/packages/designer/src/builtins/simulator/renderer/renderer.ts @@ -3,7 +3,6 @@ import { render as reactRender } from 'react-dom'; import { host } from './host'; import SimulatorRendererView from './renderer-view'; import { computed, obx } from '@recore/obx'; -import { RootSchema, NpmInfo } from '../../../designer/schema'; import { getClientRects } from '../../../utils/get-client-rects'; import { Asset } from '../utils/asset'; import loader from '../utils/loader'; @@ -13,6 +12,7 @@ import { NodeInstance } from '../../../designer/simulator'; import { isElement } from '../../../utils/is-element'; import cursor from '../../../designer/helper/cursor'; import { setNativeSelection } from '../../../designer/helper/navtive-selection'; +import { RootSchema, NpmInfo } from '../../../../../globals/src'; export class SimulatorRenderer { readonly isSimulatorRenderer = true; diff --git a/packages/designer/src/builtins/simulator/utils/parse-props.ts b/packages/designer/src/builtins/simulator/utils/parse-props.ts index 5d314c8d1..90c0ddf06 100644 --- a/packages/designer/src/builtins/simulator/utils/parse-props.ts +++ b/packages/designer/src/builtins/simulator/utils/parse-props.ts @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { isValidElement } from 'react'; import { isElement } from '../../../utils/is-element'; -import { PropType, PropConfig } from '../../../designer/prop-config'; +import { PropConfig } from '../../../../../globals'; export const primitiveTypes = [ 'string', diff --git a/packages/designer/src/designer/component-meta.ts b/packages/designer/src/designer/component-meta.ts index 0ec9329f5..b2b86f1dc 100644 --- a/packages/designer/src/designer/component-meta.ts +++ b/packages/designer/src/designer/component-meta.ts @@ -1,96 +1,24 @@ -import { ReactNode, ReactElement, ComponentType, createElement } from 'react'; import Node, { NodeParent } from './document/node/node'; -import { NodeData, NodeSchema } from './schema'; -import { PropConfig } from './prop-config'; import Designer from './designer'; -import { Remove, Clone } from '../../../globals'; -import { computed } from '@recore/obx'; +import { + IconRemove, + IconClone, + IconPage, + IconContainer, + IconComponent, + ComponentMetadata, + NpmInfo, + NodeData, + NodeSchema, + ComponentAction, + TitleContent, + TransformedComponentMetadata, + getRegisteredMetadataTransducers, + registerMetadataTransducer, + computed, +} from '../../../globals'; import { intl } from '../locale'; -export interface NestingRule { - childWhitelist?: string[]; - parentWhitelist?: string[]; -} - -export interface Configure { - props?: any[]; - styles?: object; - events?: object; - component?: { - isContainer?: boolean; - isModal?: boolean; - descriptor?: string; - nestingRule?: NestingRule; - rectSelector?: string; - // copy,move,delete - disableBehaviors?: string[]; - actions?: ComponentAction[]; - }; -} - -export interface ContentObject { - // 图标 - icon?: string | ComponentType<any> | ReactElement; - // 描述 - description?: string; - // 执行动作 - action?: (node: Node) => void; -} - -export interface ComponentAction { - // behaviorName - name: string; - // 菜单名称 - content: string | ReactNode | ContentObject; - // 子集 - items?: ComponentAction[]; - // 不显示 - condition?: boolean | ((node: Node) => boolean); - // 显示在工具条上 - important?: boolean; -} - -export function isContentObject(obj: any): obj is ContentObject { - return obj && typeof obj === 'object'; -} - -export interface ComponentMetadata { - componentName: string; - /** - * unique id - */ - uri?: string; - /** - * title or description - */ - title?: string; - /** - * svg icon for component - */ - icon?: string | ReactNode; - tags?: string[]; - description?: string; - docUrl?: string; - screenshot?: string; - devMode?: 'procode' | 'lowcode'; - npm?: { - package: string; - exportName: string; - subName: string; - main: string; - destructuring: boolean; - version: string; - }; - props?: PropConfig[]; - configure?: any[] | Configure; -} - -interface TransformedComponentMetadata extends ComponentMetadata { - configure: Configure & { - combined?: any[]; - }; -} - function ensureAList(list?: string | string[]): string[] | null { if (!list) { return null; @@ -136,22 +64,16 @@ function npmToURI(npm: { return uri; } -export type MetadataTransducer = (prev: TransformedComponentMetadata) => TransformedComponentMetadata; -const metadataTransducers: MetadataTransducer[] = []; - -// propsParser -// - -export function registerMetadataTransducer(transducer: MetadataTransducer) { - metadataTransducers.push(transducer); -} - export class ComponentMeta { readonly isComponentMeta = true; private _uri?: string; get uri(): string { return this._uri!; } + private _npm?: NpmInfo; + get npm() { + return this._npm; + } private _componentName?: string; get componentName(): string { return this._componentName!; @@ -172,10 +94,6 @@ export class ComponentMeta { get rectSelector(): string | undefined { return this._rectSelector; } - private _acceptable?: boolean; - get acceptable(): boolean { - return this._acceptable!; - } private _transformedMetadata?: TransformedComponentMetadata; get configure() { const config = this._transformedMetadata?.configure; @@ -185,20 +103,30 @@ export class ComponentMeta { private parentWhitelist?: string[] | null; private childWhitelist?: string[] | null; + private _title?: TitleContent; get title() { - return this._metadata.title || this.componentName; + return this._title || this.componentName; } get icon() { - return this._metadata.icon; + return ( + this._transformedMetadata?.icon || + (this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent) + ); } - constructor(readonly designer: Designer, private _metadata: ComponentMetadata) { - this.parseMetadata(_metadata); + private _acceptable?: boolean; + get acceptable(): boolean { + return this._acceptable!; + } + + constructor(readonly designer: Designer, metadata: ComponentMetadata) { + this.parseMetadata(metadata); } private parseMetadata(metadta: ComponentMetadata) { - const { componentName, uri, npm, props } = metadta; + const { componentName, uri, npm } = metadta; + this._npm = npm; this._uri = uri || (npm ? npmToURI(npm) : componentName); this._componentName = componentName; @@ -206,6 +134,15 @@ export class ComponentMeta { // 额外转换逻辑 this._transformedMetadata = this.transformMetadata(metadta); + const title = this._transformedMetadata.title; + if (title && typeof title === 'string') { + this._title = { + type: 'i18n', + 'en-US': this.componentName, + 'zh-CN': title, + }; + } + const { configure = {} } = this._transformedMetadata; this._acceptable = false; @@ -227,7 +164,7 @@ export class ComponentMeta { } private transformMetadata(metadta: ComponentMetadata): TransformedComponentMetadata { - const result = metadataTransducers.reduce((prevMetadata, current) => { + const result = getRegisteredMetadataTransducers().reduce((prevMetadata, current) => { return current(prevMetadata); }, preprocessMetadata(metadta)); @@ -253,13 +190,12 @@ export class ComponentMeta { return actions; } - set metadata(metadata: ComponentMetadata) { - this._metadata = metadata; + setMetadata(metadata: ComponentMetadata) { this.parseMetadata(metadata); } - get metadata(): ComponentMetadata { - return this._metadata; + getMetadata(): TransformedComponentMetadata { + return this._transformedMetadata!; } checkNestingUp(my: Node | NodeData, parent: NodeParent) { @@ -340,7 +276,7 @@ const builtinComponentActions: ComponentAction[] = [ { name: 'remove', content: { - icon: Remove, + icon: IconRemove, description: intl('remove'), action(node: Node) { node.remove(); @@ -351,7 +287,7 @@ const builtinComponentActions: ComponentAction[] = [ { name: 'copy', content: { - icon: Clone, + icon: IconClone, description: intl('copy'), action(node: Node) { // node.remove(); diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index 732b48fe2..34eeeaca7 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -1,8 +1,6 @@ -import { ComponentType as ReactComponentType } from 'react'; -import { obx, computed, autorun } from '@recore/obx'; +import { ComponentType } from 'react'; import BuiltinSimulatorView from '../builtins/simulator'; import Project from './project'; -import { ProjectSchema, NpmInfo } from './schema'; import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './helper/dragon'; import ActiveTracker from './helper/active-tracker'; import Hovering from './helper/hovering'; @@ -10,11 +8,12 @@ import Location, { LocationData, isLocationChildrenDetail } from './helper/locat import DocumentModel from './document/document-model'; import Node, { insertChildren } from './document/node/node'; import { isRootNode } from './document/node/root-node'; -import { ComponentMetadata, ComponentMeta, ComponentAction } from './component-meta'; +import { ComponentMeta } from './component-meta'; import Scroller, { IScrollable } from './helper/scroller'; import { INodeSelector } from './simulator'; import OffsetObserver, { createOffsetObserver } from './helper/offset-observer'; import { EventEmitter } from 'events'; +import { ProjectSchema, ComponentMetadata, ComponentAction, NpmInfo, obx, computed, autorun } from '../../../globals'; export interface DesignerProps { className?: string; @@ -22,8 +21,8 @@ export interface DesignerProps { defaultSchema?: ProjectSchema; hotkeys?: object; simulatorProps?: object | ((document: DocumentModel) => object); - simulatorComponent?: ReactComponentType<any>; - dragGhostComponent?: ReactComponentType<any>; + simulatorComponent?: ComponentType<any>; + dragGhostComponent?: ComponentType<any>; suspensed?: boolean; componentMetadatas?: ComponentMetadata[]; eventPipe?: EventEmitter; @@ -165,6 +164,9 @@ export default class Designer { */ createLocation(locationData: LocationData): Location { const loc = new Location(locationData); + if (this._dropLocation && this._dropLocation.document !== loc.document) { + this._dropLocation.document.internalSetDropLocation(null); + } this._dropLocation = loc; loc.document.internalSetDropLocation(loc); this.activeTracker.track({ node: loc.target, detail: loc.detail }); @@ -258,9 +260,9 @@ export default class Designer { return this.props ? this.props[key] : null; } - @obx.ref private _simulatorComponent?: ReactComponentType<any>; + @obx.ref private _simulatorComponent?: ComponentType<any>; - @computed get simulatorComponent(): ReactComponentType<any> { + @computed get simulatorComponent(): ComponentType<any> { return this._simulatorComponent || BuiltinSimulatorView; } @@ -300,12 +302,12 @@ export default class Designer { const key = data.componentName; let meta = this._componentMetasMap.get(key); if (meta) { - meta.metadata = data; + meta.setMetadata(data); } else { meta = this._lostComponentMetasMap.get(key); if (meta) { - meta.metadata = data; + meta.setMetadata(data); this._lostComponentMetasMap.delete(key); } else { meta = new ComponentMeta(this, data); @@ -342,7 +344,9 @@ export default class Designer { @computed get componentsMap(): { [key: string]: NpmInfo } { const maps: any = {}; this._componentMetasMap.forEach((config, key) => { - maps[key] = config.metadata.npm; + if (config.npm) { + maps[key] = config.npm; + } }); return maps; } diff --git a/packages/designer/src/designer/document/document-model.ts b/packages/designer/src/designer/document/document-model.ts index ca0726b76..7a244bd54 100644 --- a/packages/designer/src/designer/document/document-model.ts +++ b/packages/designer/src/designer/document/document-model.ts @@ -1,14 +1,22 @@ import Project from '../project'; -import { RootSchema, NodeData, isDOMText, isJSExpression, NodeSchema } from '../schema'; import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './node/node'; import { Selection } from './selection'; import RootNode from './node/root-node'; -import { ISimulator, Component } from '../simulator'; -import { computed, obx, autorun } from '@recore/obx'; +import { ISimulator } from '../simulator'; import Location from '../helper/location'; import { ComponentMeta } from '../component-meta'; import History from '../helper/history'; import Prop from './node/props/prop'; +import { + RootSchema, + NodeData, + isJSExpression, + isDOMText, + NodeSchema, + computed, + obx, + autorun, +} from '../../../../globals'; export default class DocumentModel { /** diff --git a/packages/designer/src/designer/document/document-view.tsx b/packages/designer/src/designer/document/document-view.tsx index f4fab1e7c..859d00186 100644 --- a/packages/designer/src/designer/document/document-view.tsx +++ b/packages/designer/src/designer/document/document-view.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; import DocumentModel from './document-model'; -import { observer } from '@recore/obx-react'; +import { observer } from '../../../../globals'; import classNames from 'classnames'; @observer diff --git a/packages/designer/src/designer/document/node/node-children.ts b/packages/designer/src/designer/document/node/node-children.ts index 7ae7b9d7b..232ef10fe 100644 --- a/packages/designer/src/designer/document/node/node-children.ts +++ b/packages/designer/src/designer/document/node/node-children.ts @@ -1,6 +1,5 @@ import Node, { NodeParent } from './node'; -import { NodeData, isNodeSchema } from '../../schema'; -import { obx, computed } from '@recore/obx'; +import { NodeData, isNodeSchema, obx, computed } from '../../../../../globals'; export default class NodeChildren { @obx.val private children: Node[]; @@ -20,7 +19,7 @@ export default class NodeChildren { return this.children.map(node => node.export(serialize)); } - import(data?: NodeData | NodeData[], checkId: boolean = false) { + import(data?: NodeData | NodeData[], checkId = false) { data = data ? (Array.isArray(data) ? data : [data]) : []; const originChildren = this.children.slice(); @@ -59,10 +58,14 @@ export default class NodeChildren { return this.size < 1; } + @computed notEmpty() { + return this.size > 0; + } + /** * 删除一个节点 */ - delete(node: Node, purge: boolean = false): boolean { + delete(node: Node, purge = false): boolean { const i = this.children.indexOf(node); if (i < 0) { return false; diff --git a/packages/designer/src/designer/document/node/node.ts b/packages/designer/src/designer/document/node/node.ts index 1c46e1f2e..035e68d77 100644 --- a/packages/designer/src/designer/document/node/node.ts +++ b/packages/designer/src/designer/document/node/node.ts @@ -1,10 +1,19 @@ -import { obx, computed } from '@recore/obx'; -import { NodeSchema, NodeData, PropsMap, PropsList, isDOMText, isJSExpression } from '../../schema'; import Props, { EXTRA_KEY_PREFIX } from './props/props'; import DocumentModel from '../document-model'; import NodeChildren from './node-children'; import Prop from './props/prop'; import { ComponentMeta } from '../../component-meta'; +import { + isDOMText, + isJSExpression, + NodeSchema, + PropsMap, + PropsList, + NodeData, + TitleContent, + obx, + computed, +} from '../../../../../globals'; /** * 基础节点 @@ -74,7 +83,7 @@ export default class Node { return -1; } - @computed get title(): string { + @computed get title(): TitleContent { let t = this.getExtraProp('title'); if (!t && this.componentMeta.descriptor) { t = this.getProp(this.componentMeta.descriptor, false); @@ -181,6 +190,30 @@ export default class Node { return this.props.export(true).props || null; } + isContainer() { + return this.isNodeParent && this.componentMeta.isContainer; + } + + @computed isSlotContainer() { + for (const item of this.props) { + if (item.type === 'slot') { + return true; + } + } + return false; + } + + @computed get slots() { + // TODO: optimize recore/obx, array maked every time, donot as changed + const slots: Node[] = []; + this.props.forEach(item => { + if (item.type === 'slot') { + slots.push(item.slotNode!); + } + }); + return slots; + } + private _conditionGroup: string | null = null; /** * 条件组 diff --git a/packages/designer/src/designer/document/node/props/prop-stash.ts b/packages/designer/src/designer/document/node/props/prop-stash.ts index 84f0f8759..d7b63347f 100644 --- a/packages/designer/src/designer/document/node/props/prop-stash.ts +++ b/packages/designer/src/designer/document/node/props/prop-stash.ts @@ -1,4 +1,4 @@ -import { obx, autorun, untracked, computed } from '@recore/obx'; +import { obx, autorun, untracked, computed } from '../../../../../../globals'; import Prop, { IPropParent, UNSET } from './prop'; import Props from './props'; diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index a8b941339..c8463c6cc 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -1,12 +1,20 @@ -import { untracked, computed, obx } from '@recore/obx'; import { valueToSource } from '../../../../utils/value-to-source'; -import { CompositeValue, isJSExpression, isJSSlot, NodeSchema, NodeData, isNodeSchema } from '../../../schema'; import PropStash from './prop-stash'; import { uniqueId } from '../../../../../../utils/unique-id'; import { isPlainObject } from '../../../../../../utils/is-plain-object'; import { hasOwnProperty } from '../../../../utils/has-own-property'; import Props from './props'; import Node from '../node'; +import { + CompositeValue, + isJSExpression, + isJSSlot, + NodeData, + isNodeSchema, + untracked, + computed, + obx, +} from '../../../../../../globals'; export const UNSET = Symbol.for('unset'); export type UNSET = typeof UNSET; @@ -197,6 +205,9 @@ export default class Prop implements IPropParent { } private _slotNode?: Node; + get slotNode() { + return this._slotNode; + } setAsSlot(data: NodeData) { this._type = 'slot'; if ( diff --git a/packages/designer/src/designer/document/node/props/props.ts b/packages/designer/src/designer/document/node/props/props.ts index 328d1315b..d46b86789 100644 --- a/packages/designer/src/designer/document/node/props/props.ts +++ b/packages/designer/src/designer/document/node/props/props.ts @@ -1,9 +1,8 @@ -import { computed, obx } from '@recore/obx'; import { uniqueId } from '../../../../../../utils/unique-id'; -import { CompositeValue, PropsList, PropsMap } from '../../../schema'; import PropStash from './prop-stash'; import Prop, { IPropParent, UNSET } from './prop'; import Node from '../node'; +import { PropsMap, PropsList, CompositeValue, computed, obx } from '../../../../../../globals'; export const EXTRA_KEY_PREFIX = '__'; @@ -279,6 +278,12 @@ export default class Props implements IPropParent { }); } + filter(fn: (item: Prop, key: number | string | undefined) => boolean) { + return this.items.filter(item => { + return fn(item, item.key); + }); + } + private purged = false; /** * 回收销毁 diff --git a/packages/designer/src/designer/document/node/root-node.ts b/packages/designer/src/designer/document/node/root-node.ts index 0bb809f33..042ecf821 100644 --- a/packages/designer/src/designer/document/node/root-node.ts +++ b/packages/designer/src/designer/document/node/root-node.ts @@ -1,8 +1,7 @@ import Node, { NodeParent } from './node'; -import { RootSchema } from '../../schema'; import DocumentModel from '../document-model'; import NodeChildren from './node-children'; -import Props from './props/props'; +import { RootSchema } from '../../../../../globals'; /** * 根容器节点 diff --git a/packages/designer/src/designer/document/selection.ts b/packages/designer/src/designer/document/selection.ts index c481b1bce..3ff2bc604 100644 --- a/packages/designer/src/designer/document/selection.ts +++ b/packages/designer/src/designer/document/selection.ts @@ -1,5 +1,5 @@ import Node, { comparePosition, PositionNO } from './node/node'; -import { obx } from '@recore/obx'; +import { obx } from '../../../../globals'; import DocumentModel from './document-model'; import { EventEmitter } from 'events'; diff --git a/packages/designer/src/designer/helper/dragon.ts b/packages/designer/src/designer/helper/dragon.ts index 232fb66f4..ca748caee 100644 --- a/packages/designer/src/designer/helper/dragon.ts +++ b/packages/designer/src/designer/helper/dragon.ts @@ -1,13 +1,12 @@ import { EventEmitter } from 'events'; -import { obx } from '@recore/obx'; import Location from './location'; import DocumentModel from '../document/document-model'; -import { NodeSchema } from '../schema'; import { ISimulator, isSimulator, ComponentInstance } from '../simulator'; import Node from '../document/node/node'; import Designer from '../designer'; import { setNativeSelection } from './navtive-selection'; import cursor from './cursor'; +import { NodeSchema, obx } from '../../../../globals'; export interface LocateEvent { readonly type: 'LocateEvent'; diff --git a/packages/designer/src/designer/helper/history.ts b/packages/designer/src/designer/helper/history.ts index 8ab0fb624..f0a3a27d9 100644 --- a/packages/designer/src/designer/helper/history.ts +++ b/packages/designer/src/designer/helper/history.ts @@ -1,7 +1,5 @@ import { EventEmitter } from 'events'; -import Session from './session'; -import { autorun, Reaction, untracked } from '@recore/obx'; -import { NodeSchema } from '../schema'; +import { NodeSchema, autorun, Reaction, untracked } from '../../../../globals'; // TODO: cache to localStorage @@ -176,3 +174,48 @@ export default class History { this.records = []; } } + +class Session { + private _data: any; + private activedTimer: any; + + get data() { + return this._data; + } + + constructor(readonly cursor: number, data: any, private timeGap: number = 1000) { + this.setTimer(); + this.log(data); + } + + log(data: any) { + if (!this.isActive()) { + return; + } + this._data = data; + this.setTimer(); + } + + isActive() { + return this.activedTimer != null; + } + + end() { + if (this.isActive()) { + this.clearTimer(); + console.info('session end'); + } + } + + private setTimer() { + this.clearTimer(); + this.activedTimer = setTimeout(() => this.end(), this.timeGap); + } + + private clearTimer() { + if (this.activedTimer) { + clearTimeout(this.activedTimer); + } + this.activedTimer = null; + } +} diff --git a/packages/designer/src/designer/helper/hovering.ts b/packages/designer/src/designer/helper/hovering.ts index a4f62abda..56e1efde8 100644 --- a/packages/designer/src/designer/helper/hovering.ts +++ b/packages/designer/src/designer/helper/hovering.ts @@ -1,9 +1,9 @@ -import { obx } from '@recore/obx'; import Node from '../document/node/node'; import DocumentModel from '../document/document-model'; +import { obx } from '../../../../globals'; export default class Hovering { - @obx.ref private _enable: boolean = true; + @obx.ref private _enable = true; get enable() { return this._enable; } @@ -13,7 +13,7 @@ export default class Hovering { this._current = null; } } - @obx.ref xRayMode: boolean = false; + @obx.ref xRayMode = false; @obx.ref private _current: Node | null = null; get current() { diff --git a/packages/designer/src/designer/helper/offset-observer.ts b/packages/designer/src/designer/helper/offset-observer.ts index 798ec43a8..39f3c4e23 100644 --- a/packages/designer/src/designer/helper/offset-observer.ts +++ b/packages/designer/src/designer/helper/offset-observer.ts @@ -1,4 +1,4 @@ -import { obx, computed } from '@recore/obx'; +import { obx, computed } from '../../../../globals'; import { INodeSelector, IViewport } from '../simulator'; import { uniqueId } from '../../../../utils/unique-id'; import { isRootNode } from '../document/node/root-node'; diff --git a/packages/designer/src/designer/helper/session.ts b/packages/designer/src/designer/helper/session.ts deleted file mode 100644 index 8095d525d..000000000 --- a/packages/designer/src/designer/helper/session.ts +++ /dev/null @@ -1,44 +0,0 @@ -export default class Session { - private _data: any; - private activedTimer: any; - - get data() { - return this._data; - } - - constructor(readonly cursor: number, data: any, private timeGap: number = 1000) { - this.setTimer(); - this.log(data); - } - - log(data: any) { - if (!this.isActive()) { - return; - } - this._data = data; - this.setTimer(); - } - - isActive() { - return this.activedTimer != null; - } - - end() { - if (this.isActive()) { - this.clearTimer(); - console.info('session end'); - } - } - - private setTimer() { - this.clearTimer(); - this.activedTimer = setTimeout(() => this.end(), this.timeGap); - } - - private clearTimer() { - if (this.activedTimer) { - clearTimeout(this.activedTimer); - } - this.activedTimer = null; - } -} diff --git a/packages/designer/src/designer/project-view.tsx b/packages/designer/src/designer/project-view.tsx index 52e11ee3b..284d9c0dc 100644 --- a/packages/designer/src/designer/project-view.tsx +++ b/packages/designer/src/designer/project-view.tsx @@ -1,7 +1,7 @@ import { Component } from 'react'; -import { observer } from '@recore/obx-react'; import Designer from './designer'; import DocumentView from './document/document-view'; +import { observer } from '../../../globals'; @observer export default class ProjectView extends Component<{ designer: Designer }> { diff --git a/packages/designer/src/designer/project.ts b/packages/designer/src/designer/project.ts index 5b5ecde9e..e64610e2f 100644 --- a/packages/designer/src/designer/project.ts +++ b/packages/designer/src/designer/project.ts @@ -1,8 +1,7 @@ -import { obx, computed } from '@recore/obx'; -import { ProjectSchema, RootSchema } from './schema'; import { EventEmitter } from 'events'; import Designer from './designer'; import DocumentModel, { isDocumentModel } from './document/document-model'; +import { ProjectSchema, RootSchema, obx, computed } from '../../../globals'; export default class Project { private emitter = new EventEmitter(); @@ -21,10 +20,12 @@ export default class Project { componentsTree: [], ...schema, }; - this.open(this.data.componentsTree[0] || { - componentName: 'Page', - fileName: '', - }); + this.open( + this.data.componentsTree[0] || { + componentName: 'Page', + fileName: '', + }, + ); } @computed get currentDocument() { @@ -106,7 +107,7 @@ export default class Project { } checkExclusive(actived: DocumentModel) { - this.documents.forEach((doc) => { + this.documents.forEach(doc => { if (doc !== actived) { doc.suspense(); } @@ -115,7 +116,7 @@ export default class Project { } closeOthers(opened: DocumentModel) { - this.documents.forEach((doc) => { + this.documents.forEach(doc => { if (doc !== opened) { doc.close(); } @@ -131,5 +132,4 @@ export default class Project { // 通知标记删除,需要告知服务端 // 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁, // 哪个删除就 - } diff --git a/packages/designer/src/designer/schema.ts b/packages/designer/src/designer/schema.ts deleted file mode 100644 index 1dab9fe82..000000000 --- a/packages/designer/src/designer/schema.ts +++ /dev/null @@ -1,158 +0,0 @@ -// 表达式 -export interface JSExpression { - type: 'JSExpression'; - /** - * 表达式字符串 - */ - value: string; - /** - * 模拟值 - */ - mock?: any; -} - -export interface JSSlot { - type: 'JSSlot'; - value: NodeSchema; -} - -// JSON 基本类型 -export type JSONValue = boolean | string | number | null | JSONArray | JSONObject; -export type JSONArray = JSONValue[]; -export interface JSONObject { - [key: string]: JSONValue; -} - -// 复合类型 -export type CompositeValue = JSONValue | JSExpression | JSSlot | CompositeArray | CompositeObject; -export type CompositeArray = CompositeValue[]; -export interface CompositeObject { - [key: string]: CompositeValue; -} - -export interface NpmInfo { - componentName?: string; - package: string; - version: string; - destructuring?: boolean; - exportName?: string; - subName?: string; - main?: string; -} - -export type ComponentsMap = NpmInfo[]; - -export type UtilsMap = Array< -| { - name: string; - type: 'npm'; - content: NpmInfo; -} -| { - name: string; - type: ''; -} ->; - -// lang "en-US" | "zh-CN" | "zh-TW" | ... -export interface I18nMap { - [lang: string]: { [key: string]: string }; -} - -export interface DataSourceConfig { - id: string; - isInit: boolean; - type: string; - options: { - uri: string; - [option: string]: CompositeValue; - }; - [otherKey: string]: CompositeValue; -} - -export interface NodeSchema { - id?: string; - componentName: string; - props?: PropsMap | PropsList; - leadingComponents?: string; - condition?: CompositeValue; - loop?: CompositeValue; - loopArgs?: [string, string]; - children?: NodeData | NodeData[]; -} - -export type PropsMap = CompositeObject; -export type PropsList = Array<{ - spread?: boolean; - name?: string; - value: CompositeValue; -}>; - -export type NodeData = NodeSchema | JSExpression | DOMText; - -export function isJSExpression(data: any): data is JSExpression { - return data && data.type === 'JSExpression'; -} - -export function isJSSlot(data: any): data is JSSlot { - return data && data.type === 'JSSlot'; -} - -export function isDOMText(data: any): data is DOMText { - return typeof data === 'string'; -} - -export type DOMText = string; - -export interface RootSchema extends NodeSchema { - componentName: string; // 'Block' | 'Page' | 'Component'; - fileName: string; - meta?: object; - state?: { - [key: string]: CompositeValue; - }; - methods?: { - [key: string]: JSExpression; - }; - lifeCycles?: { - [key: string]: JSExpression; - }; - css?: string; - dataSource?: { - items: DataSourceConfig[]; - } | any; - defaultProps?: CompositeObject; -} - -export interface BlockSchema extends RootSchema { - componentName: 'Block'; -} - -export interface PageSchema extends RootSchema { - componentName: 'Page'; -} - -export interface ComponentSchema extends RootSchema { - componentName: 'Component'; -} - -export interface ProjectSchema { - version: string; - componentsMap: ComponentsMap; - componentsTree: RootSchema[]; - i18n?: I18nMap; - utils?: UtilsMap; - constants?: JSONObject; - css?: string; - dataSource?: { - items: DataSourceConfig[]; - }; -} - -export function isNodeSchema(data: any): data is NodeSchema { - return data && data.componentName; -} - -export function isProjectSchema(data: any): data is ProjectSchema { - return data && data.componentsTree; -} diff --git a/packages/designer/src/designer/simulator.ts b/packages/designer/src/designer/simulator.ts index 99c73ad47..de27ceb7c 100644 --- a/packages/designer/src/designer/simulator.ts +++ b/packages/designer/src/designer/simulator.ts @@ -1,9 +1,9 @@ import { Component as ReactComponent, ComponentType } from 'react'; -import { LocateEvent, ISensor } from './helper/dragon'; +import { ISensor } from './helper/dragon'; import { Point } from './helper/location'; import Node from './document/node/node'; import { ScrollTarget, IScrollable } from './helper/scroller'; -import { ComponentMetadata } from './component-meta'; +import { ComponentMetadata } from '../../../globals'; export type AutoFit = '100%'; export const AutoFit = '100%'; diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 22e76df35..a16cd4b4e 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -8,7 +8,7 @@ import components from './config/components'; import utils from './config/utils'; import constants from './config/constants'; import './config/locale'; -import './config/setters'; +import '../../plugin-setters'; import './global.scss'; import './config/theme.scss'; diff --git a/packages/globals/README.md b/packages/globals/README.md index 070f5ca3b..30c3e7d7d 100644 --- a/packages/globals/README.md +++ b/packages/globals/README.md @@ -1 +1,3 @@ shared globals + + 发 CDN diff --git a/packages/globals/package.json b/packages/globals/package.json index 26761d9ba..cfedbc382 100644 --- a/packages/globals/package.json +++ b/packages/globals/package.json @@ -13,6 +13,8 @@ }, "dependencies": { "@alifd/next": "^1.19.16", + "@recore/obx": "^1.0.8", + "@recore/obx-react": "^1.0.7", "classnames": "^2.2.6", "react": "^16", "react-dom": "^16.7.0" diff --git a/packages/globals/src/components/tip/embed-tip.tsx b/packages/globals/src/components/tip/embed-tip.tsx index fd8b3d4f8..203c8acd0 100644 --- a/packages/globals/src/components/tip/embed-tip.tsx +++ b/packages/globals/src/components/tip/embed-tip.tsx @@ -1,13 +1,7 @@ import { uniqueId } from '../../../../utils/unique-id'; -import { Component, ReactNode } from 'react'; +import { Component } from 'react'; import { saveTips } from './tip-handler'; - -export interface TipConfig { - className?: string; - children?: ReactNode; - theme?: string; - direction?: string; // 'n|s|w|e|top|bottom|left|right'; -} +import { TipConfig } from '../../types'; export default class EmbedTip extends Component<TipConfig> { private id = uniqueId('tips$'); diff --git a/packages/globals/src/components/tip/tip-handler.ts b/packages/globals/src/components/tip/tip-handler.ts index 2f71d2ab3..ed1cb908a 100644 --- a/packages/globals/src/components/tip/tip-handler.ts +++ b/packages/globals/src/components/tip/tip-handler.ts @@ -1,5 +1,5 @@ -import { TipConfig } from './embed-tip'; import { EventEmitter } from 'events'; +import { TipConfig } from '../../types'; export interface TipOptions extends TipConfig { target: HTMLElement; diff --git a/packages/globals/src/components/tip/tip.tsx b/packages/globals/src/components/tip/tip.tsx index 6c91e1e97..20d6c7928 100644 --- a/packages/globals/src/components/tip/tip.tsx +++ b/packages/globals/src/components/tip/tip.tsx @@ -1,7 +1,8 @@ import { Component } from 'react'; import classNames from 'classnames'; import { resolvePosition } from './utils'; -import tipHandler from './tip-handler'; +import tipHandler, { TipOptions } from './tip-handler'; +import { intl } from '../../intl'; export default class Tip extends Component { private dispose?: () => void; @@ -100,7 +101,7 @@ export default class Tip extends Component { } render() { - const tip: any = tipHandler.tip || {}; + const tip: TipOptions = tipHandler.tip || ({} as any); const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null); this.originClassName = className; @@ -113,7 +114,7 @@ export default class Tip extends Component { }} > <i className="lc-arrow" /> - <div className="lc-tip-content">{tip.children}</div> + <div className="lc-tip-content">{intl(tip.children)}</div> </div> ); } diff --git a/packages/globals/src/components/title/index.tsx b/packages/globals/src/components/title/index.tsx index 226a5f15e..526b0ce23 100644 --- a/packages/globals/src/components/title/index.tsx +++ b/packages/globals/src/components/title/index.tsx @@ -1,27 +1,19 @@ -import { Component, isValidElement, ReactElement, ReactNode } from 'react'; -import { Icon } from '@alifd/next'; +import { Component, isValidElement } from 'react'; import classNames from 'classnames'; -import EmbedTip, { TipConfig } from '../tip/embed-tip'; +import EmbedTip from '../tip/embed-tip'; import './title.less'; -import { IconConfig, createIcon } from '../../utils'; +import { createIcon } from '../../utils'; +import { TitleContent, isI18nData } from '../../types'; +import { intl } from '../../intl'; -export interface TitleConfig { - label?: ReactNode; - tip?: string | ReactElement | TipConfig; - icon?: string | ReactElement | IconConfig; - className?: string; -} - -export type TitleContent = string | ReactElement | TitleConfig; - -export class Title extends Component<{ title: TitleContent; onClick?: () => void }> { +export class Title extends Component<{ title: TitleContent; className?: string; onClick?: () => void }> { render() { - let { title } = this.props; + let { title, className, onClick } = this.props; if (isValidElement(title)) { return title; } - if (typeof title === 'string') { - title = { label: title }; // tslint:disable-line + if (typeof title === 'string' || isI18nData(title)) { + title = { label: title }; } const icon = title.icon ? createIcon(title.icon) : null; @@ -32,22 +24,24 @@ export class Title extends Component<{ title: TitleContent; onClick?: () => void tip = title.tip; } else { const tipProps = - typeof title.tip === 'object' && !isValidElement(title.tip) ? title.tip : { children: title.tip }; + typeof title.tip === 'object' && !(isValidElement(title.tip) || isI18nData(title.tip)) + ? title.tip + : { children: title.tip }; tip = <EmbedTip direction="top" theme="black" {...tipProps} />; } } return ( - <div - className={classNames('lc-title', title.className, { + <span + className={classNames('lc-title', className, title.className, { 'has-tip': !!tip, })} - onClick={this.props.onClick} + onClick={onClick} > - {icon ? <div className="lc-title-icon">{icon}</div> : null} - {title.label ? <span className="lc-title-label">{title.label}</span> : null} + {icon ? <b className="lc-title-icon">{icon}</b> : null} + {title.label ? intl(title.label) : null} {tip} - </div> + </span> ); } } diff --git a/packages/globals/src/components/title/title.less b/packages/globals/src/components/title/title.less index 049626e6c..822b4c138 100644 --- a/packages/globals/src/components/title/title.less +++ b/packages/globals/src/components/title/title.less @@ -13,7 +13,8 @@ text-decoration-style: dashed; text-decoration-color: rgba(31, 56, 88, .3); } - padding: 2px 0; + line-height: initial !important; + word-break: break-all; } .actived .lc-title { diff --git a/packages/globals/src/di/index.ts b/packages/globals/src/di/index.ts new file mode 100644 index 000000000..734eff6fd --- /dev/null +++ b/packages/globals/src/di/index.ts @@ -0,0 +1,2 @@ +export * from './setter'; +export * from './transducer'; diff --git a/packages/globals/src/di/setter.ts b/packages/globals/src/di/setter.ts new file mode 100644 index 000000000..3e8abbd24 --- /dev/null +++ b/packages/globals/src/di/setter.ts @@ -0,0 +1,43 @@ +import { ReactNode } from 'react'; +import { CustomView, isCustomView } from '../types/setter-config'; +import { createContent } from '../../../utils/create-content'; +import { TitleContent } from '../types'; + +export type RegisteredSetter = { + component: CustomView; + defaultProps?: object; + title?: TitleContent; +}; + +const settersMap = new Map<string, RegisteredSetter>(); +export function registerSetter(type: string, setter: CustomView | RegisteredSetter) { + if (isCustomView(setter)) { + setter = { + component: setter, + title: (setter as any).displayName || (setter as any).name || 'CustomSetter' + }; + } + settersMap.set(type, setter); +} + +export function getSetter(type: string): RegisteredSetter | null { + return settersMap.get(type) || null; +} + +export function createSetterContent(setter: any, props: object): ReactNode { + if (typeof setter === 'string') { + setter = getSetter(setter); + if (!setter) { + return null; + } + if (setter.defaultProps) { + props = { + ...setter.defaultProps, + ...props, + }; + } + setter = setter.component; + } + + return createContent(setter, props); +} diff --git a/packages/globals/src/di/transducer.ts b/packages/globals/src/di/transducer.ts new file mode 100644 index 000000000..c10639e41 --- /dev/null +++ b/packages/globals/src/di/transducer.ts @@ -0,0 +1,12 @@ +import { TransformedComponentMetadata } from '../types'; + +export type MetadataTransducer = (prev: TransformedComponentMetadata) => TransformedComponentMetadata; +const metadataTransducers: MetadataTransducer[] = []; + +export function registerMetadataTransducer(transducer: MetadataTransducer) { + metadataTransducers.push(transducer); +} + +export function getRegisteredMetadataTransducers(): MetadataTransducer[] { + return metadataTransducers; +} diff --git a/packages/globals/src/icons/clone.tsx b/packages/globals/src/icons/clone.tsx index 57dd701db..60b6b0dfc 100644 --- a/packages/globals/src/icons/clone.tsx +++ b/packages/globals/src/icons/clone.tsx @@ -1,6 +1,6 @@ import { IconBase, IconBaseProps } from "./icon-base"; -export function Clone(props: IconBaseProps) { +export function IconClone(props: IconBaseProps) { return ( <IconBase viewBox="0 0 1024 1024" {...props}> <path d="M192 256.16C192 220.736 220.704 192 256.16 192h639.68C931.264 192 960 220.704 960 256.16v639.68A64.16 64.16 0 0 1 895.84 960H256.16A64.16 64.16 0 0 1 192 895.84V256.16z m64 31.584v576.512a32 32 0 0 0 31.744 31.744h576.512a32 32 0 0 0 31.744-31.744V287.744A32 32 0 0 0 864.256 256H287.744A32 32 0 0 0 256 287.744zM288 192v64h64V192H288z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m96 96v64h64V288h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m-96 96v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64H288z m-96-96v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64V288H192z m160 416c0-17.664 14.592-32 32.064-32h319.872a31.968 31.968 0 1 1 0 64h-319.872A31.968 31.968 0 0 1 352 704z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 576z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 448z m512 47.936V192h-64V159.968A31.776 31.776 0 0 0 768.032 128H160A31.776 31.776 0 0 0 128 159.968V768c0 17.92 14.304 31.968 31.968 31.968H192v64h303.936H128.128A63.968 63.968 0 0 1 64 799.872V128.128C64 92.704 92.48 64 128.128 64h671.744C835.296 64 864 92.48 864 128.128v367.808z"/> @@ -8,4 +8,4 @@ export function Clone(props: IconBaseProps) { ); } -Clone.displayName = 'Clone'; +IconClone.displayName = 'Clone'; diff --git a/packages/globals/src/icons/component.tsx b/packages/globals/src/icons/component.tsx new file mode 100644 index 000000000..c389eac39 --- /dev/null +++ b/packages/globals/src/icons/component.tsx @@ -0,0 +1,10 @@ +import { IconBase, IconBaseProps } from "./icon-base"; + +export function IconComponent(props: IconBaseProps) { + return ( + <IconBase viewBox="0 0 1024 1024" {...props}> + <path d="M783.5648 437.4528h-18.0224V336.6912c0-43.8272-35.6352-79.4624-79.4624-79.4624h-110.592V241.664c0-90.9312-73.728-164.6592-164.6592-164.6592-90.9312 0-164.6592 73.728-164.6592 164.6592v15.5648H155.2384c-43.8272 0-79.4624 35.6352-79.4624 79.4624v131.4816c0 16.7936 13.9264 30.72 30.72 30.72h56.1152c56.9344 0 103.2192 46.2848 103.2192 103.2192s-46.2848 103.2192-103.2192 103.2192H106.496c-16.7936 0-30.72 13.9264-30.72 30.72v131.4816c0 43.8272 35.6352 79.4624 79.4624 79.4624h531.2512c43.8272 0 79.4624-35.6352 79.4624-79.4624v-100.7616h18.0224c90.9312 0 164.6592-73.728 164.6592-164.6592-0.4096-90.9312-74.1376-164.6592-165.0688-164.6592z m0 267.8784h-48.7424c-16.7936 0-30.72 13.9264-30.72 30.72v131.4816c0 9.8304-8.192 18.0224-18.0224 18.0224H155.2384c-9.8304 0-18.0224-8.192-18.0224-18.0224v-100.7616h25.3952c90.9312 0 164.6592-73.728 164.6592-164.6592 0-90.9312-73.728-164.6592-164.6592-164.6592h-25.3952V336.6912c0-9.8304 8.192-18.0224 18.0224-18.0224h121.6512c16.7936 0 30.72-13.9264 30.72-30.72V241.664c0-56.9344 46.2848-103.2192 103.2192-103.2192s103.2192 46.2848 103.2192 103.2192v46.2848c0 16.7936 13.9264 30.72 30.72 30.72h141.312c9.8304 0 18.0224 8.192 18.0224 18.0224v131.4816c0 16.7936 13.9264 30.72 30.72 30.72h48.7424c56.9344 0 103.2192 46.2848 103.2192 103.2192s-46.2848 103.2192-103.2192 103.2192z" /> + </IconBase> + ); +} +IconComponent.displayName = 'Component'; diff --git a/packages/globals/src/icons/container.tsx b/packages/globals/src/icons/container.tsx new file mode 100644 index 000000000..ceff81183 --- /dev/null +++ b/packages/globals/src/icons/container.tsx @@ -0,0 +1,11 @@ +import { IconBase, IconBaseProps } from "./icon-base"; + +export function IconContainer(props: IconBaseProps) { + return ( + <IconBase viewBox="0 0 1024 1024" {...props}> + <path d="M800 800h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-256 0h64v64h-64v-64z m0-640h64v64h-64v-64z m128 640h64v64h-64v-64zM160 672h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m640 384h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z" /> + <path d="M896 64H128c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V128c0-35.2-28.8-64-64-64z m0 800c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V160c0-19.2 12.8-32 32-32h704c19.2 0 32 12.8 32 32v704z" /> + </IconBase> + ); +} +IconContainer.displayName = 'Container'; diff --git a/packages/globals/src/icons/hidden.tsx b/packages/globals/src/icons/hidden.tsx index 3abdc4448..5df9004cb 100644 --- a/packages/globals/src/icons/hidden.tsx +++ b/packages/globals/src/icons/hidden.tsx @@ -1,10 +1,10 @@ import { IconBase, IconBaseProps } from "./icon-base"; -export function Hidden(props: IconBaseProps) { +export function IconHidden(props: IconBaseProps) { return ( <IconBase viewBox="0 0 1024 1024" {...props}> <path d="M183.423543 657.078213l163.499771-98.484012c-4.233418-14.908548-6.646374-30.585599-6.646374-46.852074 0-94.665033 76.739778-171.404812 171.404812-171.404812 45.983287 0 87.641059 18.20871 118.42518 47.679929l129.791042-78.17957c-73.254398-41.73145-157.866471-65.812915-248.216221-65.812915-192.742792 0-360.068705 108.505249-444.453604 267.715321C96.636944 567.228859 136.301316 616.355743 183.423543 657.078213zM841.253886 367.552144l-164.382884 99.015108c3.934612 14.415314 6.215562 29.513174 6.215562 45.174875 0 94.665033-76.739778 171.404812-171.404812 171.404812-45.361117 0-86.484723-17.747199-117.142977-46.515407l-129.419582 77.955466c72.874751 41.149189 156.893306 64.871473 246.563582 64.871473 192.742792 0 360.068705-108.505249 444.453604-267.717368C927.000805 456.773188 887.794875 408.054603 841.253886 367.552144zM420.280042 511.741104c0 0.550539 0.152473 1.060145 0.161682 1.608637l135.080511-81.366146c-13.065574-7.198959-27.854395-11.658528-43.826158-11.658528C461.20922 420.325068 420.280042 461.254246 420.280042 511.741104zM447.739441 576.947198l69.02098-41.574884L948.364369 275.395234c10.812253-6.512321 14.297634-20.558222 7.785314-31.369452-6.512321-10.812253-20.556175-14.296611-31.368428-7.785314L575.654762 446.537056l0 0-151.20577 91.078345 0 0L75.027787 748.090043c-10.812253 6.512321-14.297634 20.556175-7.785314 31.368428 6.512321 10.812253 20.556175 14.297634 31.369452 7.785314L447.739441 576.947198 447.739441 576.947198zM511.696078 603.157139c50.487881 0 91.416036-40.928155 91.416036-91.416036 0-0.549515-0.152473-1.057075-0.161682-1.605567l-135.079488 81.364099C480.935494 598.699618 495.724315 603.157139 511.696078 603.157139z" /> </IconBase> ); } -Hidden.displayName = 'Hidden'; +IconHidden.displayName = 'Hidden'; diff --git a/packages/globals/src/icons/index.ts b/packages/globals/src/icons/index.ts index f843f3a52..ac8767dc0 100644 --- a/packages/globals/src/icons/index.ts +++ b/packages/globals/src/icons/index.ts @@ -1,5 +1,8 @@ export * from './clone'; export * from './hidden'; export * from './remove'; -export * from './settings'; +export * from './setting'; export * from './icon-base'; +export * from './component'; +export * from './container'; +export * from './page'; diff --git a/packages/globals/src/icons/page.tsx b/packages/globals/src/icons/page.tsx new file mode 100644 index 000000000..6fd182bf4 --- /dev/null +++ b/packages/globals/src/icons/page.tsx @@ -0,0 +1,12 @@ +import { IconBase, IconBaseProps } from "./icon-base"; + +export function IconPage(props: IconBaseProps) { + return ( + <IconBase viewBox="0 0 1024 1024" {...props}> + <path d="M381.6 864H162.4c-6.9 0-12.4 4.6-12.4 10.3v19.3c0 5.7 5.6 10.3 12.4 10.3h219.1c6.8 0 12.4-4.6 12.4-10.3v-19.3c0.1-5.7-5.5-10.3-12.3-10.3zM382 780.6H162c-6.9 0-12.5 4.6-12.5 10.3v19.3c0 5.7 5.6 10.3 12.5 10.3h220c6.9 0 12.5-4.6 12.5-10.3v-19.3c0-5.7-5.6-10.3-12.5-10.3zM162.4 737.2h219.1c6.8 0 12.4-4.6 12.4-10.3v-19.3c0-5.7-5.6-10.3-12.4-10.3H162.4c-6.9 0-12.4 4.6-12.4 10.3v19.3c0 5.7 5.6 10.3 12.4 10.3z" /> + <path d="M977.1 0H46.9C21 0 0 21 0 46.9v930.2c0 25.9 21 46.9 46.9 46.9h930.2c25.9 0 46.9-21 46.9-46.9V46.9C1024 21 1003 0 977.1 0z m-18.7 911.6c0 25.9-21 46.9-46.9 46.9H112.4c-25.9 0-46.9-21-46.9-47V112.4c0-25.9 21-46.9 46.9-46.9h799.1c25.9 0 46.9 21 46.9 46.9v799.2z" /> + <path d="M207.9 342.7h608.2c32 0 57.9-25.9 57.9-57.9v-83c0-32-25.9-57.9-57.9-57.9H207.9c-32 0-57.9 25.9-57.9 57.9v83c0 32 25.9 57.9 57.9 57.9zM200 201.8c0-4.4 3.5-7.9 7.9-7.9h608.2c4.4 0 7.9 3.5 7.9 7.9v83c0 4.4-3.5 7.9-7.9 7.9H207.9c-4.4 0-7.9-3.5-7.9-7.9v-83zM806.4 405.7h-277c-37.3 0-67.6 30.2-67.6 67.6v363.2c0 37.3 30.2 67.6 67.6 67.6h277c37.3 0 67.6-30.2 67.6-67.6V473.3c0-37.4-30.2-67.6-67.6-67.6zM824 836.4c0 9.7-7.9 17.6-17.6 17.6h-277c-9.7 0-17.6-7.9-17.6-17.6V473.3c0-9.7 7.9-17.6 17.6-17.6h277c9.7 0 17.6 7.9 17.6 17.6v363.1zM272 649.7c67.4 0 122-54.6 122-122s-54.6-122-122-122-122 54.6-122 122 54.6 122 122 122z m0-204c45.2 0 82 36.8 82 82s-36.8 82-82 82-82-36.8-82-82 36.8-82 82-82z" /> + </IconBase> + ); +} +IconPage.displayName = 'Page'; diff --git a/packages/globals/src/icons/remove.tsx b/packages/globals/src/icons/remove.tsx index 9f90088e7..5caf18f9e 100644 --- a/packages/globals/src/icons/remove.tsx +++ b/packages/globals/src/icons/remove.tsx @@ -1,10 +1,10 @@ import { IconBase, IconBaseProps } from './icon-base'; -export function Remove(props: IconBaseProps) { +export function IconRemove(props: IconBaseProps) { return ( <IconBase viewBox="0 0 1024 1024" {...props}> <path d="M224 256v639.84A64 64 0 0 0 287.84 960h448.32A64 64 0 0 0 800 895.84V256h64a32 32 0 1 0 0-64H160a32 32 0 1 0 0 64h64zM384 96c0-17.664 14.496-32 31.904-32h192.192C625.696 64 640 78.208 640 96c0 17.664-14.496 32-31.904 32H415.904A31.872 31.872 0 0 1 384 96z m-96 191.744C288 270.208 302.4 256 320.224 256h383.552C721.6 256 736 270.56 736 287.744v576.512C736 881.792 721.6 896 703.776 896H320.224A32.224 32.224 0 0 1 288 864.256V287.744zM352 352c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z" /> </IconBase> ); } -Remove.displayName = 'Remove'; +IconRemove.displayName = 'Remove'; diff --git a/packages/globals/src/icons/settings.tsx b/packages/globals/src/icons/setting.tsx similarity index 96% rename from packages/globals/src/icons/settings.tsx rename to packages/globals/src/icons/setting.tsx index 05628a7c1..e555cdf43 100644 --- a/packages/globals/src/icons/settings.tsx +++ b/packages/globals/src/icons/setting.tsx @@ -1,6 +1,6 @@ import { IconBase, IconBaseProps } from './icon-base'; -export function Setting(props: IconBaseProps) { +export function IconSetting(props: IconBaseProps) { return ( <IconBase viewBox="0 0 1024 1024" {...props}> <path d="M965.824 405.952a180.48 180.48 0 0 1-117.12-85.376 174.464 174.464 0 0 1-16-142.08 22.208 22.208 0 0 0-7.04-23.552 480.576 480.576 0 0 0-153.6-89.216 23.104 23.104 0 0 0-24.32 5.76 182.208 182.208 0 0 1-135.68 57.92 182.208 182.208 0 0 1-133.12-56.64 23.104 23.104 0 0 0-26.88-7.04 478.656 478.656 0 0 0-153.6 89.856 22.208 22.208 0 0 0-7.04 23.552 174.464 174.464 0 0 1-16 141.44A180.48 180.48 0 0 1 58.24 405.952a22.4 22.4 0 0 0-17.28 17.792 455.08 455.08 0 0 0 0 176.512 22.4 22.4 0 0 0 17.28 17.792 180.48 180.48 0 0 1 117.12 84.736c25.408 42.944 31.232 94.592 16 142.08a22.208 22.208 0 0 0 7.04 23.552A480.576 480.576 0 0 0 352 957.632h7.68a23.04 23.04 0 0 0 16.64-7.04 184.128 184.128 0 0 1 266.944 0c6.592 8.96 18.752 11.968 28.8 7.04a479.36 479.36 0 0 0 156.16-88.576 22.208 22.208 0 0 0 7.04-23.552 174.464 174.464 0 0 1 13.44-142.72 180.48 180.48 0 0 1 117.12-84.736 22.4 22.4 0 0 0 17.28-17.792 452.613 452.613 0 0 0 0-176.512 23.04 23.04 0 0 0-17.28-17.792z m-42.88 169.408a218.752 218.752 0 0 0-128 98.112 211.904 211.904 0 0 0-21.76 156.736 415.936 415.936 0 0 1-112 63.68 217.472 217.472 0 0 0-149.12-63.68 221.312 221.312 0 0 0-149.12 63.68 414.592 414.592 0 0 1-112-63.68c12.8-53.12 4.288-109.12-23.68-156.096A218.752 218.752 0 0 0 101.12 575.36a386.176 386.176 0 0 1 0-127.36 218.752 218.752 0 0 0 128-98.112c27.2-47.552 34.944-103.68 21.76-156.8a415.296 415.296 0 0 1 112-63.68A221.44 221.44 0 0 0 512 187.392a218.24 218.24 0 0 0 149.12-57.984 413.952 413.952 0 0 1 112 63.744 211.904 211.904 0 0 0 23.04 156.096 218.752 218.752 0 0 0 128 98.112 386.65 386.65 0 0 1 0 127.36l-1.28 0.64z" /> @@ -9,4 +9,4 @@ export function Setting(props: IconBaseProps) { ); } -Setting.displayName = 'Setting'; +IconSetting.displayName = 'Setting'; diff --git a/packages/globals/src/index.ts b/packages/globals/src/index.ts index bbef70f75..cd2456f92 100644 --- a/packages/globals/src/index.ts +++ b/packages/globals/src/index.ts @@ -2,3 +2,6 @@ export * from './intl'; export * from './components'; export * from './utils'; export * from './icons'; +export * from './types'; +export * from './di'; +export * from './obx'; diff --git a/packages/globals/src/intl/ali-global-locale.ts b/packages/globals/src/intl/ali-global-locale.ts index 37d97a115..9737a68ba 100644 --- a/packages/globals/src/intl/ali-global-locale.ts +++ b/packages/globals/src/intl/ali-global-locale.ts @@ -36,6 +36,9 @@ class AliGlobalLocale { } setLocale(locale: string) { + if (locale === this.locale) { + return; + } this.locale = locale; if (hasLocalStorage(window)) { const store = window.localStorage; @@ -54,6 +57,7 @@ class AliGlobalLocale { store.setItem(LowcodeConfigKey, JSON.stringify(config)); } + this.emitter.emit('localechange', locale); } getLocale() { diff --git a/packages/globals/src/intl/index.tsx b/packages/globals/src/intl/index.tsx index 1f54012f7..47064b08e 100644 --- a/packages/globals/src/intl/index.tsx +++ b/packages/globals/src/intl/index.tsx @@ -1,5 +1,6 @@ import { globalLocale } from './ali-global-locale'; import { PureComponent, ReactNode } from 'react'; +import { isI18nData } from '../types'; function injectVars(template: string, params: any): string { if (!template || !params) { @@ -13,14 +14,20 @@ function injectVars(template: string, params: any): string { return $1; }); } - -export interface I18nData { - type: 'i18n'; - [key: string]: string; -} - -export function isI18nData(obj: any): obj is I18nData { - return obj && obj.type === 'i18n'; +function generateTryLocales(locale: string) { + const tries = [locale, locale.replace('-', '_')]; + if (locale === 'zh-TW' || locale === 'en-US') { + tries.push('zh-CN'); + tries.push('zh_CN'); + } else { + tries.push('en-US'); + tries.push('en_US'); + if (locale !== 'zh-CN') { + tries.push('zh-CN'); + tries.push('zh_CN'); + } + } + return tries; } export function localeFormat(data: any, params?: object): string { @@ -28,9 +35,16 @@ export function localeFormat(data: any, params?: object): string { return data; } const locale = globalLocale.getLocale(); - const tpl = data[locale]; + const tries = generateTryLocales(locale); + let tpl: string | undefined; + for (const lan of tries) { + tpl = data[lan]; + if (tpl != null) { + break; + } + } if (tpl == null) { - return `##intl null@${locale}##`; + return `##intl@${locale}##`; } return injectVars(tpl, params); } @@ -53,11 +67,22 @@ export function intl(data: any, params?: object): ReactNode { return data; } +export function shallowIntl(data: any): any { + if (!data || typeof data !== 'object') { + return data; + } + const maps: any = {}; + Object.keys(data).forEach(key => { + maps[key] = localeFormat(data[key]); + }); + return maps; +} + export function createIntl( instance: string | object, ): { intl(id: string, params?: object): ReactNode; - getIntlString(id: string, params?: object): string; + intlString(id: string, params?: object): string; getLocale(): string; setLocale(locale: string): void; } { @@ -79,11 +104,12 @@ export function createIntl( useLocale(globalLocale.getLocale()); - function getIntlString(key: string, params?: object): string { + function intlString(key: string, params?: object): string { + // TODO: tries lost language const str = data[key]; if (str == null) { - return `##intl null@${key}##`; + return `##intl@${key}##`; } return injectVars(str, params); @@ -101,7 +127,7 @@ export function createIntl( } render() { const { id, params } = this.props; - return getIntlString(id, params); + return intlString(id, params); } } @@ -109,7 +135,7 @@ export function createIntl( intl(id: string, params?: object) { return <Intl id={id} params={params} />; }, - getIntlString, + intlString, getLocale() { return globalLocale.getLocale(); }, diff --git a/packages/globals/src/obx/index.ts b/packages/globals/src/obx/index.ts new file mode 100644 index 000000000..3ab051f08 --- /dev/null +++ b/packages/globals/src/obx/index.ts @@ -0,0 +1,4 @@ +export * from '@recore/obx'; +import { observer } from '@recore/obx-react'; + +export { observer }; diff --git a/packages/globals/src/types/data-source.ts b/packages/globals/src/types/data-source.ts new file mode 100644 index 000000000..79bca2e91 --- /dev/null +++ b/packages/globals/src/types/data-source.ts @@ -0,0 +1,16 @@ +import { CompositeValue } from './value-type'; + +export interface DataSourceConfig { + id: string; + isInit: boolean; + type: string; + options: { + uri: string; + [option: string]: CompositeValue; + }; + [otherKey: string]: CompositeValue; +} + +export interface DataSource { + items: DataSourceConfig[]; +} diff --git a/packages/globals/src/types/field-config.ts b/packages/globals/src/types/field-config.ts new file mode 100644 index 000000000..b0e4380bc --- /dev/null +++ b/packages/globals/src/types/field-config.ts @@ -0,0 +1,58 @@ +import { TitleContent } from './title'; +import { SetterType } from './setter-config'; + + +export interface FieldExtraProps { + /** + * 是否必填参数 + */ + isRequired?: boolean; + /** + * default value of target prop for setter use + */ + defaultValue?: any; + getValue?: (field: any, fieldValue: any) => any; + setValue?: (field: any, value: any) => void; + /** + * the field conditional show, is not set always true + * @default undefined + */ + condition?: (field: any) => boolean; + /** + * default collapsed when display accordion + */ + defaultCollapsed?: boolean; + /** + * important field + */ + important?: boolean; + /** + * internal use + */ + forceInline?: number; +} + +export interface FieldConfig extends FieldExtraProps { + type?: 'field' | 'group'; + /** + * the name of this setting field, which used in quickEditor + */ + name: string | number; + /** + * the field title + * @default sameas .name + */ + title?: TitleContent; + /** + * the field body contains when .type = 'field' + */ + setter?: SetterType; + /** + * the setting items which group body contains when .type = 'group' + */ + items?: FieldConfig[]; + /** + * extra props for field + */ + extraProps?: FieldExtraProps; +} diff --git a/packages/globals/src/types/i18n.ts b/packages/globals/src/types/i18n.ts new file mode 100644 index 000000000..8a3f6e558 --- /dev/null +++ b/packages/globals/src/types/i18n.ts @@ -0,0 +1,13 @@ +export interface I18nData { + type: 'i18n'; + [key: string]: string; +} + +// type checks +export function isI18nData(obj: any): obj is I18nData { + return obj && obj.type === 'i18n'; +} + +export interface I18nMap { + [lang: string]: { [key: string]: string }; +} diff --git a/packages/globals/src/types/icon.ts b/packages/globals/src/types/icon.ts new file mode 100644 index 000000000..2f2ba2336 --- /dev/null +++ b/packages/globals/src/types/icon.ts @@ -0,0 +1,9 @@ +import { ReactElement, ComponentType } from 'react'; + +export interface IconConfig { + type: string; + size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit'; + className?: string; +} + +export type IconType = string | ReactElement | ComponentType<any> | IconConfig; diff --git a/packages/globals/src/types/index.ts b/packages/globals/src/types/index.ts new file mode 100644 index 000000000..705f7b28a --- /dev/null +++ b/packages/globals/src/types/index.ts @@ -0,0 +1,13 @@ +export * from './data-source'; +export * from './field-config'; +export * from './i18n'; +export * from './icon'; +export * from './metadata'; +export * from './npm'; +export * from './prop-config'; +export * from './schema'; +export * from './tip'; +export * from './title'; +export * from './utils'; +export * from './value-type'; +export * from './setter-config'; diff --git a/packages/globals/src/types/metadata.ts b/packages/globals/src/types/metadata.ts new file mode 100644 index 000000000..d0e36270c --- /dev/null +++ b/packages/globals/src/types/metadata.ts @@ -0,0 +1,87 @@ +import { ReactNode } from 'react'; +import { IconType } from './icon'; +import { TipContent } from './tip'; +import { TitleContent } from './title'; +import { PropConfig } from './prop-config'; +import { NpmInfo } from './npm'; +import { FieldConfig } from './field-config'; + +export interface NestingRule { + childWhitelist?: string[]; + parentWhitelist?: string[]; + descendantBlacklist?: string[]; + ancestorWhitelist?: string[]; +} + +export interface ComponentConfigure { + isContainer?: boolean; + isModal?: boolean; + isNullNode?: boolean; + descriptor?: string; + nestingRule?: NestingRule; + rectSelector?: string; + // copy,move,delete + disableBehaviors?: string[]; + actions?: ComponentAction[]; +} + +export interface Configure { + props?: FieldConfig[]; + styles?: object; + events?: object; + component?: ComponentConfigure; +} + +export interface ActionContentObject { + // 图标 + icon?: IconType; + // 描述 + description?: TipContent; + // 执行动作 + action?: (node: any) => void; +} + +export interface ComponentAction { + // behaviorName + name: string; + // 菜单名称 + content: string | ReactNode | ActionContentObject; + // 子集 + items?: ComponentAction[]; + // 不显示 + condition?: boolean | ((node: any) => boolean); + // 显示在工具条上 + important?: boolean; +} + +export function isActionContentObject(obj: any): obj is ActionContentObject { + return obj && typeof obj === 'object'; +} + +export interface ComponentMetadata { + componentName: string; + /** + * unique id + */ + uri?: string; + /** + * title or description + */ + title?: TitleContent; + /** + * svg icon for component + */ + icon?: IconType; + tags?: string[]; + description?: string; + docUrl?: string; + screenshot?: string; + devMode?: 'procode' | 'lowcode'; + npm?: NpmInfo; + props?: PropConfig[]; + configure?: FieldConfig[] | Configure; +} + +export interface TransformedComponentMetadata extends ComponentMetadata { + configure: Configure & { combined?: FieldConfig[] }; +} diff --git a/packages/globals/src/types/npm.ts b/packages/globals/src/types/npm.ts new file mode 100644 index 000000000..0335c3c81 --- /dev/null +++ b/packages/globals/src/types/npm.ts @@ -0,0 +1,11 @@ +export interface NpmInfo { + componentName?: string; + package: string; + version: string; + destructuring?: boolean; + exportName?: string; + subName?: string; + main?: string; +} + +export type ComponentsMap = NpmInfo[]; diff --git a/packages/designer/src/designer/prop-config.ts b/packages/globals/src/types/prop-config.ts similarity index 100% rename from packages/designer/src/designer/prop-config.ts rename to packages/globals/src/types/prop-config.ts diff --git a/packages/globals/src/types/schema.ts b/packages/globals/src/types/schema.ts new file mode 100644 index 000000000..74f8d9e74 --- /dev/null +++ b/packages/globals/src/types/schema.ts @@ -0,0 +1,80 @@ +import { ComponentsMap } from './npm'; +import { CompositeValue, JSExpression, CompositeObject, JSONObject } from './value-type'; +import { DataSource } from './data-source'; +import { I18nMap } from './i18n'; +import { UtilsMap } from './utils'; + +export interface NodeSchema { + id?: string; + componentName: string; + props?: PropsMap | PropsList; + leadingComponents?: string; + condition?: CompositeValue; + loop?: CompositeValue; + loopArgs?: [string, string]; + children?: NodeData | NodeData[]; +} + +export type PropsMap = CompositeObject; +export type PropsList = Array<{ + spread?: boolean; + name?: string; + value: CompositeValue; +}>; + +export type NodeData = NodeSchema | JSExpression | DOMText; + +export function isDOMText(data: any): data is DOMText { + return typeof data === 'string'; +} + +export type DOMText = string; + +export interface RootSchema extends NodeSchema { + componentName: string; // 'Block' | 'Page' | 'Component'; + fileName: string; + meta?: object; + state?: { + [key: string]: CompositeValue; + }; + methods?: { + [key: string]: JSExpression; + }; + lifeCycles?: { + [key: string]: JSExpression; + }; + css?: string; + dataSource?: DataSource; + defaultProps?: CompositeObject; +} + +export interface BlockSchema extends RootSchema { + componentName: 'Block'; +} + +export interface PageSchema extends RootSchema { + componentName: 'Page'; +} + +export interface ComponentSchema extends RootSchema { + componentName: 'Component'; +} + +export interface ProjectSchema { + version: string; + componentsMap: ComponentsMap; + componentsTree: RootSchema[]; + i18n?: I18nMap; + utils?: UtilsMap; + constants?: JSONObject; + css?: string; + dataSource?: DataSource; +} + +export function isNodeSchema(data: any): data is NodeSchema { + return data && data.componentName; +} + +export function isProjectSchema(data: any): data is ProjectSchema { + return data && data.componentsTree; +} diff --git a/packages/globals/src/types/setter-config.ts b/packages/globals/src/types/setter-config.ts new file mode 100644 index 000000000..1df336543 --- /dev/null +++ b/packages/globals/src/types/setter-config.ts @@ -0,0 +1,34 @@ +import { isReactComponent } from '../utils'; +import { ComponentType, ReactElement, isValidElement } from 'react'; + +export type CustomView = ReactElement | ComponentType<any>; + +export type DynamicProps = (field: any) => object; + +export interface SetterConfig { + /** + * if *string* passed must be a registered Setter Name + */ + componentName: string | CustomView; + /** + * the props pass to Setter Component + */ + props?: object | DynamicProps; + children?: any; + isRequired?: boolean; + initialValue?: any | ((field: any) => any); +} + +/** + * if *string* passed must be a registered Setter Name, future support blockSchema + */ +export type SetterType = SetterConfig | string | CustomView; + + +export function isSetterConfig(obj: any): obj is SetterConfig { + return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj); +} + +export function isCustomView(obj: any): obj is CustomView { + return obj && (isValidElement(obj) || isReactComponent(obj)); +} diff --git a/packages/globals/src/types/tip.ts b/packages/globals/src/types/tip.ts new file mode 100644 index 000000000..0d9cd4752 --- /dev/null +++ b/packages/globals/src/types/tip.ts @@ -0,0 +1,11 @@ +import { I18nData } from './i18n'; +import { ReactNode, ReactElement } from 'react'; + +export interface TipConfig { + className?: string; + children?: I18nData | ReactNode; + theme?: string; + direction?: string; // 'n|s|w|e|top|bottom|left|right'; +} + +export type TipContent = string | I18nData | ReactElement | TipConfig; diff --git a/packages/globals/src/types/title.ts b/packages/globals/src/types/title.ts new file mode 100644 index 000000000..a63692933 --- /dev/null +++ b/packages/globals/src/types/title.ts @@ -0,0 +1,15 @@ +import { ReactElement, ReactNode } from 'react'; +import { I18nData } from './i18n'; +import { TipContent } from './tip'; +import { IconType } from './icon'; + + +export interface TitleConfig { + label?: I18nData | ReactNode; + tip?: TipContent; + icon?: IconType; + className?: string; +} + +export type TitleContent = string | I18nData | ReactElement | TitleConfig; + diff --git a/packages/globals/src/types/utils.ts b/packages/globals/src/types/utils.ts new file mode 100644 index 000000000..f4a925f3b --- /dev/null +++ b/packages/globals/src/types/utils.ts @@ -0,0 +1,13 @@ +import { NpmInfo } from './npm'; + +export type UtilsMap = Array< +| { + name: string; + type: 'npm'; + content: NpmInfo; + } +| { + name: string; + type: ''; + } +>; diff --git a/packages/globals/src/types/value-type.ts b/packages/globals/src/types/value-type.ts new file mode 100644 index 000000000..5659a555b --- /dev/null +++ b/packages/globals/src/types/value-type.ts @@ -0,0 +1,42 @@ +import { NodeSchema } from './schema'; + +// 表达式 +export interface JSExpression { + type: 'JSExpression'; + /** + * 表达式字符串 + */ + value: string; + /** + * 模拟值 + */ + mock?: any; +} + +export interface JSSlot { + type: 'JSSlot'; + value: NodeSchema; +} + +// JSON 基本类型 +export type JSONValue = boolean | string | number | null | JSONArray | JSONObject; +export type JSONArray = JSONValue[]; +export interface JSONObject { + [key: string]: JSONValue; +} + +// 复合类型 +export type CompositeValue = JSONValue | JSExpression | JSSlot | CompositeArray | CompositeObject; +export type CompositeArray = CompositeValue[]; +export interface CompositeObject { + [key: string]: CompositeValue; +} + + +export function isJSExpression(data: any): data is JSExpression { + return data && data.type === 'JSExpression'; +} + +export function isJSSlot(data: any): data is JSSlot { + return data && data.type === 'JSSlot'; +} diff --git a/packages/globals/src/utils/create-icon.tsx b/packages/globals/src/utils/create-icon.tsx index 728d2fcdd..0f9ec82c7 100644 --- a/packages/globals/src/utils/create-icon.tsx +++ b/packages/globals/src/utils/create-icon.tsx @@ -1,18 +1,14 @@ import { Icon } from '@alifd/next'; -import { isValidElement, ReactNode, ComponentType, createElement, cloneElement, ReactElement } from 'react'; +import { isValidElement, ReactNode, createElement, cloneElement } from 'react'; import { isReactComponent } from './is-react'; - -export interface IconConfig { - type: string; - size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit'; - className?: string; -} - -export type IconType = string | ReactElement | ComponentType<any> | IconConfig; +import { IconType } from '../types'; const URL_RE = /^(https?:)\/\//i; -export function createIcon(icon: IconType, props?: object): ReactNode { +export function createIcon(icon?: IconType | null, props?: object): ReactNode { + if (!icon) { + return null; + } if (typeof icon === 'string') { if (URL_RE.test(icon)) { return <img src={icon} {...props} />; @@ -26,9 +22,5 @@ export function createIcon(icon: IconType, props?: object): ReactNode { return createElement(icon, {...props}); } - if (icon) { - return <Icon {...icon} {...props} />; - } - - return null; + return <Icon {...icon} {...props} />; } diff --git a/packages/plugin-outline-pane/.eslintignore b/packages/plugin-outline-pane/.eslintignore new file mode 100644 index 000000000..1fb2edf7c --- /dev/null +++ b/packages/plugin-outline-pane/.eslintignore @@ -0,0 +1,6 @@ +.idea/ +.vscode/ +build/ +.* +~* +node_modules diff --git a/packages/plugin-outline-pane/.eslintrc b/packages/plugin-outline-pane/.eslintrc new file mode 100644 index 000000000..db78d35d1 --- /dev/null +++ b/packages/plugin-outline-pane/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/@recore/config/.eslintrc" +} diff --git a/packages/plugin-outline-pane/.prettierrc b/packages/plugin-outline-pane/.prettierrc new file mode 100644 index 000000000..8748c5ed3 --- /dev/null +++ b/packages/plugin-outline-pane/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json new file mode 100644 index 000000000..c34c3f4da --- /dev/null +++ b/packages/plugin-outline-pane/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ali/lowcode-plugin-outline-pane", + "version": "0.0.0", + "description": "xxx for Ali lowCode engine", + "main": "src/index.ts", + "files": [ + "lib" + ], + "scripts": { + "build": "tsc", + "test": "ava", + "test:snapshot": "ava --update-snapshots" + }, + "dependencies": { + "@alifd/next": "^1.19.16", + "classnames": "^2.2.6", + "react": "^16", + "react-dom": "^16.7.0" + }, + "devDependencies": { + "@recore/config": "^2.0.0", + "@types/classnames": "^2.2.7", + "@types/node": "^13.7.1", + "@types/react": "^16", + "@types/react-dom": "^16", + "eslint": "^6.5.1", + "prettier": "^1.18.2", + "tslib": "^1.9.3", + "typescript": "^3.1.3", + "ts-node": "^8.0.1" + }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + }, + "license": "MIT" +} diff --git a/packages/plugin-outline-pane/src/README.md b/packages/plugin-outline-pane/src/README.md new file mode 100644 index 000000000..0171bb10e --- /dev/null +++ b/packages/plugin-outline-pane/src/README.md @@ -0,0 +1 @@ +大纲树 diff --git a/packages/plugin-outline-pane/src/helper/dwell-timer.ts b/packages/plugin-outline-pane/src/helper/dwell-timer.ts new file mode 100644 index 000000000..e629018ef --- /dev/null +++ b/packages/plugin-outline-pane/src/helper/dwell-timer.ts @@ -0,0 +1,37 @@ +/** + * 停留检查计时器 + */ +export default class DwellTimer { + private timer: number | undefined; + private previous: any; + + constructor(readonly timeout: number = 400) {} + + /** + * 根据传入的 ID 判断,停留事件是否触发 + * 如果上一次的标示(包括不存在)和这次不相同,则设置停留计时器 + * 反之什么也不用做 + */ + start(id: any, fn: () => void) { + if (this.previous !== id) { + this.end(); + this.previous = id; + this.timer = setTimeout(() => { + fn(); + this.end(); + }, this.timeout) as number; + } + } + + end() { + const timer = this.timer; + if (timer) { + clearTimeout(timer); + this.timer = undefined; + } + + if (this.previous) { + this.previous = undefined; + } + } +} diff --git a/packages/plugin-outline-pane/src/helper/is-container.ts b/packages/plugin-outline-pane/src/helper/is-container.ts new file mode 100644 index 000000000..e8474ad6a --- /dev/null +++ b/packages/plugin-outline-pane/src/helper/is-container.ts @@ -0,0 +1,13 @@ +import { INode, isElementNode, isRootNode } from '../../../../document/node'; + +export function isContainer(node: INode): boolean { + if (isRootNode(node)) { + return true; + } + if (isElementNode(node)) { + // TODO: check from prototype + // block Fragment + return true; + } + return false; +} diff --git a/packages/plugin-outline-pane/src/helper/x-axis-tracker.ts b/packages/plugin-outline-pane/src/helper/x-axis-tracker.ts new file mode 100644 index 000000000..27dc58d90 --- /dev/null +++ b/packages/plugin-outline-pane/src/helper/x-axis-tracker.ts @@ -0,0 +1,111 @@ +/** + * X 轴追踪器,左右移动光标时获取正确位置 + */ +import { INode, INodeParent, isRootNode } from '../../../../document/node'; +import Location, { + isLocationChildrenDetail, + LocationChildrenDetail, + LocationData, + LocationDetailType, +} from '../../../../document/location'; +import { LocateEvent } from '../../../../globals'; +import { isContainer } from './is-container'; + +export default class XAxisTracker { + private location!: Location; + private start: number = 0; + + /** + * @param unit 移动单位 + */ + constructor(readonly unit = 15) {} + + track(loc: Location, e: LocateEvent): LocationData | null { + this.location = loc; + + if (this.start === 0) { + this.start = e.globalX; + } + + const parent = this.locate(e); + + if (!parent) { + return null; + } + + return { + target: parent as INodeParent, + detail: { + type: LocationDetailType.Children, + index: parent.children.length, + }, + }; + } + + /** + * 定位 + */ + locate(e: LocateEvent): INode | null { + if (!isLocationChildrenDetail(this.location.detail)) { + return null; + } + + const delta = e.globalX - this.start; + let direction = null; + + if (delta < 0) { + direction = 'left'; + } else { + direction = 'right'; + } + + const n = Math.floor(Math.abs(delta) / this.unit); + + // console.log('x', e.globalX, 'y', e.globalY, 'delta', delta, 'n', n, 'start', this.start); + + if (n < 1) { + return null; + } + + // 一旦移动一个单位,就将"原点"清零 + this.reset(); + + const node = this.location.target; + const index = (this.location.detail as LocationChildrenDetail).index; + let parent = null; + + if (direction === 'left') { + // 如果光标是往左运动 + // 该节点如果不是最后一个节点,那么就没有继续查找下去的必要 + // console.log('>>> [left]', index, node.children.length, node); + if (isRootNode(node)) { + return null; + } + // index 为 0 表示第一个位置 + // 第一个位置或者不是最后以为位置,都不需要处理 + if (index < node.children.length - 1) { + return null; + } + parent = node.parent as INode; + } else { + // 插入线一般是在元素下面,所以这边需要多减去 1,即 -2 + if (index === 0) { + return null; + } + const i2 = Math.max(index - 1, 0); + parent = node.children[i2]; + // console.log('>>> [right]', index, i2, parent, node.id); + } + + // parent 节点判断 + if (!parent || !isContainer(parent)) { + return null; + } + + return parent; + } + + reset() { + this.start = 0; + } +} diff --git a/packages/plugin-outline-pane/src/icons/border-outer.svg b/packages/plugin-outline-pane/src/icons/border-outer.svg new file mode 100644 index 000000000..be86f4793 --- /dev/null +++ b/packages/plugin-outline-pane/src/icons/border-outer.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562677547122" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1131" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1020.14942288-40.33632922H3.85057712c-24.44088257 0-44.18690634 19.74602377-44.18690634 44.18690634v1016.29884576c0 24.44088257 19.74602377 44.18690634 44.18690634 44.18690634h1016.29884576c24.44088257 0 44.18690634-19.74602377 44.18690634-44.18690634V3.85057712c0-24.44088257-19.74602377-44.18690634-44.18690634-44.18690634z m-55.23363292 1005.25211918H59.08421004V59.08421004h905.83157992v905.83157992z" fill="#ffffff" p-id="1132"></path><path d="M473.33645695 310.39723984h77.3270861c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672659v-77.32708608c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672658h-77.3270861c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672658v77.32708608c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672659zM222.02342717 561.71026963h77.32708608c6.07569962 0 11.04672658-4.97102697 11.04672659-11.04672658v-77.3270861c0-6.07569962-4.97102697-11.04672658-11.04672659-11.04672658h-77.32708608c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672658v77.3270861c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672658zM724.64948675 561.71026963h77.32708608c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672658v-77.3270861c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672658h-77.32708608c-6.07569962 0-11.04672658 4.97102697-11.04672659 11.04672658v77.3270861c0 6.07569962 4.97102697 11.04672658 11.04672659 11.04672658zM473.33645695 561.71026963h77.3270861c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672658v-77.3270861c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672658h-77.3270861c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672658v77.3270861c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672658zM473.33645695 813.02329941h77.3270861c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672658v-77.32708608c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672659h-77.3270861c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672659v77.32708608c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672658z" fill="#ffffff" p-id="1133"></path></svg> \ No newline at end of file diff --git a/packages/plugin-outline-pane/src/icons/caret-down.svg b/packages/plugin-outline-pane/src/icons/caret-down.svg new file mode 100644 index 000000000..68139c5eb --- /dev/null +++ b/packages/plugin-outline-pane/src/icons/caret-down.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562677537258" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="907" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M965.46812628 219.26174551H58.53187372c-27.20256421 0-42.39181327 28.72148912-25.54555523 48.3294288l453.46812628 525.82418542c12.97990373 15.05116498 37.97312263 15.05116498 51.09111046 0L991.01368151 267.59117431c16.84625804-19.6079397 1.65700898-48.3294288-25.54555523-48.3294288z" fill="#ffffff" p-id="908"></path></svg> \ No newline at end of file diff --git a/packages/plugin-outline-pane/src/icons/caret-right.svg b/packages/plugin-outline-pane/src/icons/caret-right.svg new file mode 100644 index 000000000..150293223 --- /dev/null +++ b/packages/plugin-outline-pane/src/icons/caret-right.svg @@ -0,0 +1 @@ +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562677542938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1019" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M793.41535973 486.45444477L267.59117431 32.98631849c-19.6079397-16.84625804-48.3294288-1.65700898-48.3294288 25.54555523v906.93625256c0 27.20256421 28.72148912 42.39181327 48.3294288 25.54555523l525.82418542-453.46812628c15.05116498-12.97990373 15.05116498-38.11120672 0-51.09111046z" fill="#ffffff" p-id="1020"></path></svg> \ No newline at end of file diff --git a/packages/plugin-outline-pane/src/index.ts b/packages/plugin-outline-pane/src/index.ts new file mode 100644 index 000000000..34f4e1230 --- /dev/null +++ b/packages/plugin-outline-pane/src/index.ts @@ -0,0 +1,10 @@ +import Pane from './views/pane'; + +export default { + name: 'outline-tree', + title: { + label: '大纲树', + icon: { name: 'outline', size: '14px' }, + }, + content: Pane, +}; diff --git a/packages/plugin-outline-pane/src/locale/en-US.json b/packages/plugin-outline-pane/src/locale/en-US.json new file mode 100644 index 000000000..666810c46 --- /dev/null +++ b/packages/plugin-outline-pane/src/locale/en-US.json @@ -0,0 +1,4 @@ +{ + "Designer not found": "Designer not found", + "No opened document": "No opened document", +} diff --git a/packages/plugin-outline-pane/src/locale/index.ts b/packages/plugin-outline-pane/src/locale/index.ts new file mode 100644 index 000000000..32205fb5c --- /dev/null +++ b/packages/plugin-outline-pane/src/locale/index.ts @@ -0,0 +1,10 @@ +import { createIntl } from '../../../globals'; +import en_US from './en-US.json'; +import zh_CN from './zh-CN.json'; + +const { intl, getLocale, setLocale } = createIntl({ + 'en-US': en_US, + 'zh-CN': zh_CN, +}); + +export { intl, getLocale, setLocale }; diff --git a/packages/plugin-outline-pane/src/locale/zh-CN.json b/packages/plugin-outline-pane/src/locale/zh-CN.json new file mode 100644 index 000000000..d75772bdf --- /dev/null +++ b/packages/plugin-outline-pane/src/locale/zh-CN.json @@ -0,0 +1,4 @@ +{ + "Designer not found": "未发现设计器模块", + "No opened document": "没有打开的文档" +} diff --git a/packages/plugin-outline-pane/src/main.ts b/packages/plugin-outline-pane/src/main.ts new file mode 100644 index 000000000..aa8afd27c --- /dev/null +++ b/packages/plugin-outline-pane/src/main.ts @@ -0,0 +1,101 @@ +import { computed, obx } from '../../globals'; +import Designer from '../../designer/src/designer/designer'; +import { ISensor, LocateEvent } from '../../designer/src/designer/helper/dragon'; +import { Tree } from './tree'; +import Location from '../../designer/src/designer/helper/location'; + +class TreeMaster { + constructor(readonly designer: Designer) {} + + private treeMap = new Map<string, Tree>(); + @computed get currentTree(): Tree | null { + const doc = this.designer?.currentDocument; + if (doc) { + const id = doc.id; + if (this.treeMap.has(id)) { + return this.treeMap.get(id)!; + } + const tree = new Tree(doc); + // TODO: listen purge event to remove + this.treeMap.set(id, tree); + return tree; + } + return null; + } +} + +const mastersMap = new Map<Designer, TreeMaster>(); +function getTreeMaster(designer: Designer): TreeMaster { + let master = mastersMap.get(designer); + if (!master) { + master = new TreeMaster(designer); + mastersMap.set(designer, master); + } + return master; +} + +export class OutlineMain implements ISensor { + private _designer?: Designer; + @obx.ref private _master?: TreeMaster; + get master() { + return this._master; + } + + constructor(readonly editor: any) { + if (editor.designer) { + this.setupDesigner(editor.designer); + } else { + editor.once('designer.ready', (designer: Designer) => { + this.setupDesigner(designer); + }); + } + } + + fixEvent(e: LocateEvent): LocateEvent { + throw new Error("Method not implemented."); + } + + locate(e: LocateEvent): Location | undefined { + throw new Error("Method not implemented."); + } + + isEnter(e: LocateEvent): boolean { + throw new Error("Method not implemented."); + } + + deactiveSensor(): void { + throw new Error("Method not implemented."); + } + + private setupDesigner(designer: Designer) { + this._designer = designer; + this._master = getTreeMaster(designer); + designer.dragon.addSensor(this); + } + + purge() { + this._designer?.dragon.removeSensor(this); + // todo purge treeMaster if needed + } + + private _sensorAvailable: boolean = false; + /** + * @see ISensor + */ + get sensorAvailable() { + return this._sensorAvailable; + } + + private _shell: HTMLDivElement | null = null; + mount(shell: HTMLDivElement | null) { + if (this._shell === shell) { + return; + } + this._shell = shell; + if (shell) { + this._sensorAvailable = true; + } + } +} + + diff --git a/packages/plugin-outline-pane/src/sensor.ts b/packages/plugin-outline-pane/src/sensor.ts new file mode 100644 index 000000000..a0b92615d --- /dev/null +++ b/packages/plugin-outline-pane/src/sensor.ts @@ -0,0 +1,220 @@ +import { ISenseAble, LocateEvent, isNodesDragTarget, activeTracker, getCurrentDocument } from '../../../globals'; +import Location, { isLocationChildrenDetail, LocationDetailType } from '../../../document/location'; +import tree from './tree'; +import Scroller, { ScrollTarget } from '../../../document/scroller'; +import { isShadowNode } from '../../../document/node/shadow-node'; +import TreeNode from './tree-node'; +import { INodeParent } from '../../../document/node'; +import DwellTimer from './helper/dwell-timer'; +import XAxisTracker from './helper/x-axis-tracker'; + +export const OutlineBoardID = 'outline-board'; +export default class OutlineBoard implements ISenseAble { + id = OutlineBoardID; + + get bounds() { + const rootElement = this.element; + const clientBound = rootElement.getBoundingClientRect(); + + return { + height: clientBound.height, + width: clientBound.width, + top: clientBound.top, + left: clientBound.left, + right: clientBound.right, + bottom: clientBound.bottom, + scale: 1, + scrollHeight: rootElement.scrollHeight, + scrollWidth: rootElement.scrollWidth, + }; + } + + sensitive: boolean = true; + private sensing: boolean = false; + + private scrollTarget = new ScrollTarget(this.element); + private scroller = new Scroller(this, this.scrollTarget); + + constructor(readonly element: HTMLDivElement) { + activeTracker.onChange(({ node, detail }) => { + const treeNode = isShadowNode(node) ? tree.getTreeNode(node.origin) : tree.getTreeNode(node); + if (treeNode.hidden) { + return; + } + + if (detail && detail.type === LocationDetailType.Children) { + treeNode.expand(true); + } else { + treeNode.expandParents(); + } + this.scrollToNode(treeNode, detail); + }); + } + + private tryScrollAgain: number | null = null; + scrollToNode(treeNode: TreeNode, detail?: any, tryTimes: number = 0) { + this.tryScrollAgain = null; + if (this.sensing) { + // is a active sensor + return; + } + + const opt: any = {}; + let scroll = false; + let rect: ClientRect | null; + if (detail && isLocationChildrenDetail(detail)) { + rect = tree.getInsertionRect(); + } else { + rect = treeNode.computeRect(); + } + + if (!rect) { + if (!this.tryScrollAgain && tryTimes < 3) { + this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(treeNode, detail, tryTimes + 1)); + } + return; + } + const scrollTarget = this.scrollTarget; + const st = scrollTarget.top; + const { height, top, bottom, scrollHeight } = this.bounds; + + if (rect.top < top || rect.bottom > bottom) { + opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height); + scroll = true; + } + + if (scroll && this.scroller) { + this.scroller.scrollTo(opt); + } + } + + isEnter(e: LocateEvent): boolean { + return this.inRange(e); + } + + inRange(e: LocateEvent): boolean { + const rect = this.bounds; + return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right; + } + + deactive(): void { + this.sensing = false; + console.log('>>> deactive'); + } + + fixEvent(e: LocateEvent): LocateEvent { + return e; + } + + private dwellTimer: DwellTimer = new DwellTimer(450); + private xAxisTracker = new XAxisTracker(); + + locate(e: LocateEvent): Location | undefined { + this.sensing = true; + this.scroller.scrolling(e); + + const dragTarget = e.dragTarget; + // FIXME: not support multiples/nodedatas/any data, + const dragment = isNodesDragTarget(dragTarget) ? dragTarget.nodes[0] : null; + if (!dragment) { + return; + } + const doc = getCurrentDocument()!; + const preDraggedNode = doc.dropLocation && doc.dropLocation.target; + + // 左右移动追踪,一旦左右移动满足位置条件,直接返回即可。 + if (doc.dropLocation) { + const loc2 = this.xAxisTracker.track(doc.dropLocation, e); + if (loc2) { + this.dwellTimer.end(); + return doc.createLocation(loc2); + } + } else { + this.dwellTimer.end(); + return doc.createLocation({ + target: dragment.parent!, + detail: { + type: LocationDetailType.Children, + index: dragment.index, + }, + }); + } + + // 这语句的后半段是解决"丢帧"问题 + // e 有一种情况,是从 root > .flow 开始冒泡,而不是实际节点。这种情况往往发生在:光标在插入框内移动 + // 此时取上一次插入位置的 node 即可 + const treeNode = tree.getTreeNodeByEvent(e as any) || (preDraggedNode && tree.getTreeNode(preDraggedNode)); + + // TODO: 没有判断是否可以放入 isDropContainer,决定 target 的值是父节点还是本节点 + if (!treeNode || dragment === treeNode.node || treeNode.ignored) { + this.dwellTimer.end(); + console.warn('not found tree-node or other reasons', treeNode, e); + return undefined; + } + + // console.log('I am at', treeNode.id, e); + + const rect = treeNode.computeRect(); + if (!rect) { + this.dwellTimer.end(); + console.warn('can not get the rect, node', treeNode.id); + return undefined; + } + + const node = treeNode.node; + const parentNode = node.parent; + + if (!parentNode) { + this.dwellTimer.end(); + return undefined; + } + + let index = Math.max(parentNode.children.indexOf(node), 0); + const center = rect.top + rect.height / 2; + + // 常规处理 + // 如果可以展开,但是没有展开,需要设置延时器,检查停留时间然后展开 + // 最后返回合适的位置信息 + // FIXME: 容器判断存在问题,比如 img 是可以被放入的 + if (treeNode.isContainer() && !treeNode.expanded) { + if (e.globalY > center) { + this.dwellTimer.start(treeNode.id, () => { + doc.createLocation({ + target: node as INodeParent, + detail: { + type: LocationDetailType.Children, + index: 0, + }, + }); + }); + } + } else { + this.dwellTimer.end(); + } + + // 如果节点是展开状态,并且光标是在其下方,不做任何处理,直接返回即可 + // 如果不做这个处理,那么会出现"抖动"情况:在当前元素中心线下方时,会作为该元素的第一个子节点插入,而又会碰到已经存在对第一个字节点"争相"处理 + if (treeNode.expanded) { + if (e.globalY > center) { + return undefined; + } + } + + // 如果光标移动到节点中心线下方,则将元素插入到该节点下方 + // 反之插入该节点上方 + if (e.globalY > center) { + // down + index = index + 1; + } + + index = Math.min(index, parentNode.children.length); + + return doc.createLocation({ + target: parentNode, + detail: { + type: LocationDetailType.Children, + index, + }, + }); + } +} diff --git a/packages/plugin-outline-pane/src/tree-node.ts b/packages/plugin-outline-pane/src/tree-node.ts new file mode 100644 index 000000000..d1e3a502f --- /dev/null +++ b/packages/plugin-outline-pane/src/tree-node.ts @@ -0,0 +1,233 @@ +import { computed, obx, TitleContent } from '../../globals'; +import Node from '../../designer/src/designer/document/node/node'; +import DocumentModel from '../../designer/src/designer/document/document-model'; +import { isLocationChildrenDetail } from '../../designer/src/designer/helper/location'; +import Designer from '../../designer/src/designer/designer'; +import { Tree } from './tree'; + +export interface Title { + label: string; + icon?: string; + actions?: any; +} + +export default class TreeNode { + get id(): string { + return this.node.id; + } + + /** + * 是否可以展开 + */ + @computed get expandable(): boolean { + return this.hasChildren() || this.isSlotContainer() || this.dropIndex != null; + } + + /** + * 插入"线"位置信息 + */ + @computed get dropIndex(): number | null { + const loc = this.node.document.dropLocation; + return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail.index : null; + } + + @computed get depth(): number { + return this.node.zLevel; + } + + /** + * 是否是响应投放区 + */ + @computed isResponseDropping(): boolean { + const loc = this.node.document.dropLocation; + if (!loc) { + return false; + } + return loc.target === this.node; + } + + /** + * 默认为折叠状态 + * 在初始化根节点时,设置为展开状态 + */ + @obx.ref private _expanded = false; + get expanded(): boolean { + return this.expandable && this._expanded; + } + + set expanded(value: boolean) { + this._expanded = value; + } + + @computed get hovering() { + return this.designer.hovering.current === this.node; + } + + @computed get hidden(): boolean { + return this.node.getExtraProp('hidden', false)?.getValue() === true; + } + + @computed get ignored(): boolean { + return this.node.getExtraProp('ignored', false)?.getValue() === true; + } + + @computed get locked(): boolean { + return this.node.getExtraProp('locked', false)?.getValue() === true; + } + + @computed get selected(): boolean { + // TODO: check is dragging + const selection = this.document.selection; + return selection.has(this.node.id); + } + + @computed get title(): TitleContent { + return this.node.title; + } + + @computed get parent() { + const parent = this.node.parent; + if (parent) { + return this.tree.getTreeNode(parent); + } + return null; + } + + @computed get slots(): TreeNode[] { + // todo: shallowEqual + return this.node.slots.map((node) => this.tree.getTreeNode(node)); + } + + @computed get children(): TreeNode[] | null { + return this.node.children?.map((node) => this.tree.getTreeNode(node)) || null; + } + + /** + * 是否是容器,允许子节点拖入 + */ + isContainer(): boolean { + return this.node.isContainer(); + } + + /** + * 判断是否有"插槽" + */ + isSlotContainer(): boolean { + return this.node.isSlotContainer(); + } + + hasChildren(): boolean { + return this.isContainer() && this.node.children?.notEmpty() ? true : false; + } + + /* + get xForValue() { + const node = this.node; + return isElementNode(node) && node.xforValue ? node.xforValue : null; + } + + get flowHidden() { + return (this.node as ElementNode).flowHidden; + } + + get flowIndex() { + return (this.node as ElementNode).flowIndex; + } + + get conditionFlow() { + return (this.node as ElementNode).conditionFlow; + } + + hasXIf() { + return hasConditionFlow(this.node); + } + + hasXFor() { + const node = this.node; + return isElementNode(node) && node.xforFn; + } + */ + + /** + * 展开节点,支持依次展开父节点 + */ + expand(tryExpandParents: boolean = false) { + // 这边不能直接使用 expanded,需要额外判断是否可以展开 + // 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开 + if (this.expandable && !this._expanded) { + this.expanded = true; + } + if (tryExpandParents) { + this.expandParents(); + } + } + + /** + * 光标停留处理 + * 超过一定时间,展开节点 + */ + private dwellTimer: number | undefined; + clearDwellTimer() { + clearTimeout(this.dwellTimer); + this.dwellTimer = undefined; + } + willExpand() { + if (this.dwellTimer) { + return; + } + this.clearDwellTimer(); + if (this.expanded) { + return; + } + this.dwellTimer = setTimeout(() => { + this.clearDwellTimer(); + this.expand(true); + }, 400) as any; + } + + expandParents() { + let p = this.node.parent; + while (p) { + this.tree.getTreeNode(p).expanded = true; + p = p.parent; + } + } + + private titleRef: HTMLDivElement | null = null; + mount(ref: HTMLDivElement | null) { + this.titleRef = ref; + } + + computeRect() { + let target = this.titleRef; + if (!target) { + const nodeId = this.id; + target = window.document.querySelector(`div[data-id="${nodeId}"]`); + } + return target && target.getBoundingClientRect(); + } + + select(isMulti: boolean) { + const node = this.node; + + /* + if (this.hasXIf()) { + (node as ElementNode).setFlowVisible(); + } + */ + + const selection = node.document.selection; + if (isMulti) { + selection.add(node.id); + } else { + selection.select(node.id); + } + } + + readonly designer: Designer; + readonly document: DocumentModel; + constructor(readonly tree: Tree, readonly node: Node) { + this.document = node.document; + this.designer = this.document.designer; + } +} diff --git a/packages/plugin-outline-pane/src/tree.ts b/packages/plugin-outline-pane/src/tree.ts new file mode 100644 index 000000000..cefe9493c --- /dev/null +++ b/packages/plugin-outline-pane/src/tree.ts @@ -0,0 +1,23 @@ +import TreeNode from './tree-node'; +import DocumentModel from '../../designer/src/designer/document/document-model'; +import Node from '../../designer/src/designer/document/node/node'; + +export class Tree { + private treeNodesMap = new Map<string, TreeNode>(); + + readonly root: TreeNode; + + constructor(readonly document: DocumentModel) { + this.root = this.getTreeNode(document.rootNode); + } + + getTreeNode(node: Node): TreeNode { + if (this.treeNodesMap.has(node.id)) { + return this.treeNodesMap.get(node.id)!; + } + + const treeNode = new TreeNode(this, node); + this.treeNodesMap.set(node.id, treeNode); + return treeNode; + } +} diff --git a/packages/plugin-outline-pane/src/views/pane.less b/packages/plugin-outline-pane/src/views/pane.less new file mode 100644 index 000000000..001de23f1 --- /dev/null +++ b/packages/plugin-outline-pane/src/views/pane.less @@ -0,0 +1,251 @@ +/* 面板背景的颜色 */ +@pane-bgcolor: #333131; // #1a1c23; +/* 标题背景色 */ +//@title-bgcolor: var(--pane-bg-color; // backup rgba(0, 0, 0, 0.2); +/* 标题边框色 */ +@title-bdcolor: transparent; +@title-selectedcolor: #111; +@section-bgcolor: transparent; +@section-bdcolor: rgba(0, 0, 0, 0.1); +/* 文字颜色 */ +@text-color: #ffffff; + +.hidden { + display: none; +} + +.my-outline-pane { + top: 0; + left: 0; + height: 100%; + width: 100%; + position: absolute; + + > .tree-scroll-container { + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; + overflow: auto; + } +} + +.my-outline-tree { + overflow: hidden; + margin-bottom: 20px; + padding-left: 5px; + + // 禁用 Text Select + user-select: none; + + .insertion { + pointer-events: none !important; + border: 1px dashed var(--color-brand-light); + height: 25px; + transform: translateZ(0); + } + + .condition-flow-container { + @bd-setting: 1px solid #7b605b; + border-top: @bd-setting; + border-bottom: @bd-setting; + position: relative; + + &:before { + position: absolute; + display: block; + width: 0; + border-left: @bd-setting; + height: 100%; + top: 0; + left: 0; + content: ' '; + z-index: 2; + } + } + + .tree-node { + color: rgb(217, 217, 217); + + .c-control-flow-title { + text-align: center; + background-color: #7b605b; + height: 14px; + > b { + transform: scale(0.75); + transform-origin: top; + text-shadow: 0px 0px 2px black; + display: block; + } + } + + .tree-node-collapsed-icon { + transition: transform 0.01s; + margin-left: 4px; + filter: opacity(0.5); + + & > svg { + width: 8px; + height: 8px; + } + + &:hover { + filter: opacity(1); + } + } + + .tree-node-icon { + transform: translateZ(0); + display: flex; + align-items: center; + margin-right: 5px; + margin-left: 5px; + + & > svg { + width: 16px; + height: 16px; + } + } + + .tree-node-ignored-icon { + display: none; + position: absolute; + right: 8px; + top: 6px; + } + + .tree-node-title { + font-size: var(--font-size-text); + padding: 0; + cursor: pointer; + background: var(--pane-bg-color); + display: flex; + align-items: center; + position: relative; + transform: translateZ(0); + + .tree-node-title-inner { + flex: 1; + height: 26px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: flex; + align-items: center; + } + + .tree-node-title-input { + flex: 1; + border: none; + background-color: var(--pane-bg-color); + color: var(--color-pane-label); + height: 24px; + line-height: 24px; + } + + .tree-node-title-text { + flex: 1; + color: rgb(217, 217, 217); + // problem + //width: 100%; + //height: 100%; + display: flex; + align-items: center; + + &.x-for-text { + color: #9370db; + } + + &.x-if-text { + color: #ff6308; + } + + .info { + margin-left: 10px; + } + + .editable { + display: flex; + justify-content: space-between; + } + } + } + + &.expanded { + > .tree-node-title > .tree-node-collapsed-icon { + transform-origin: center; + transform: rotate(90deg); + } + //> .branches { + // > .tree-node > .tree-node-title > .tree-node-icon { + // margin-left: 9px; + // } + //} + } + + &.hover { + & > .tree-node-title { + background: @title-selectedcolor * 1.6; + + .tree-node-ignored-icon { + display: block; + } + } + } + + // 忽略节点处理 + &.ignored { + .tree-node-title-text { + color: #9b9b9b; + } + + .tree-node-collapsed-icon, + .tree-node-ignored-icon { + display: block; + filter: opacity(0.5); + } + } + + // 选中节点处理 + &.selected { + & > .tree-node-title { + background: @title-selectedcolor; + } + } + + // 处理拖入节点 + &.dropping { + & > .tree-node-title { + background: @title-selectedcolor * 0.75; + } + + & > .branches:before { + border-left: 1px solid var(--color-brand-light); + } + } + + .branches { + padding-left: 12px; + position: relative; + + &:before { + position: absolute; + display: block; + width: 0; + border-left: 0.5px solid rgba(149, 216, 160, 0.25); + height: 100%; + top: 0; + left: 10px; + content: ' '; + z-index: 2; + } + + &.x-flow { + &:before { + border-left: 1px solid #ff6308; + } + } + } + } +} diff --git a/packages/plugin-outline-pane/src/views/pane.tsx b/packages/plugin-outline-pane/src/views/pane.tsx new file mode 100644 index 000000000..f698611be --- /dev/null +++ b/packages/plugin-outline-pane/src/views/pane.tsx @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; +import { OutlineMain } from '../main'; +import { intl } from '../locale'; +import { observer } from '../../../globals/src'; +import './style.less'; + +@observer +export default class OutlinePane extends Component<{ editor: any }> { + private main = new OutlineMain(this.props.editor); + + shouldComponentUpdate() { + return false; + } + + componentWillUnmount() { + this.main.purge(); + } + + render() { + if (!this.main.master) { + return ( + <div className="lc-outline-pane"> + <p className="lc-outline-notice">{intl('Designer not found')}</p> + </div> + ); + } + + if (!this.main.master.currentTree) { + return ( + <div className="lc-outline-pane"> + <p className="lc-outline-notice">{intl('No opened document')}</p> + </div> + ); + } + + return ( + <div className="lc-outline-pane"> + <div + ref={shell => this.main.mount(shell)} + className="lc-outline-tree-container" + > + <TreeView tree={this.main.master.currentTree} /> + </div> + </div> + ); + } +} diff --git a/packages/plugin-outline-pane/src/views/tree-branches.tsx b/packages/plugin-outline-pane/src/views/tree-branches.tsx new file mode 100644 index 000000000..c60df167a --- /dev/null +++ b/packages/plugin-outline-pane/src/views/tree-branches.tsx @@ -0,0 +1,56 @@ +import { observer } from '../../../globals/src'; + +@observer +export default class TreeBranches extends Component<TreeNodeProps> { + shouldComponentUpdate() { + return false; + } + + render() { + const treeNode = this.props.treeNode; + const { expanded } = treeNode; + + if (!expanded) { + return null; + } + + const branchClassName = classNames({ + branches: !isRootNode(treeNode.node), + // 'x-branch': treeNode.hasXIf() && treeNode.branchIndex !== treeNode.branchNode!.children.length - 1, + }); + + let children: any = []; + + if (treeNode.hasChildren() /* || node.hasSlots() */) { + children = treeNode.children.map((child: TreeNode) => { + if (child.hasXIf()) { + if (child.flowIndex === 0) { + const conditionFlowContainer = classNames('condition-group-container', { + hidden: child.hidden, + }); + return ( + <div key={child.id} className={conditionFlowContainer} data-id={child.id}> + <div className="c-control-flow-title"><b>Condition Flow</b></div> + {child.conditionGroup!.children.map(c => { + return <TreeNodeView key={c.id} treeNode={tree.getTreeNode(c)} />; + })} + </div> + ); + } else { + return null; + } + } + return <TreeNodeView key={child.id} treeNode={child} />; + }); + } + if (treeNode.dropIndex != null) { + children.splice( + treeNode.dropIndex, + 0, + <div key="insertion" ref={ref => tree.mountInsertion(ref)} className="insertion" />, + ); + } + + return children.length > 0 && <div className={branchClassName}>{children}</div>; + } +} diff --git a/packages/plugin-outline-pane/src/views/tree-node-icon-view.tsx b/packages/plugin-outline-pane/src/views/tree-node-icon-view.tsx new file mode 100644 index 000000000..6b87e60dc --- /dev/null +++ b/packages/plugin-outline-pane/src/views/tree-node-icon-view.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import DIVIcon from 'my-icons/container.svg'; +import IMGIcon from 'my-icons/image.svg'; +import { observer } from '@ali/recore'; + +@observer +export default class TreeNodeIconView extends React.Component<{ tagName: string }> { + shouldComponentUpdate(): boolean { + return false; + } + + render() { + const { tagName } = this.props; + switch (tagName) { + case 'img': { + console.log('>>> tag:', tagName); + return <IMGIcon />; + } + default: + return <DIVIcon />; + } + } +} diff --git a/packages/plugin-outline-pane/src/views/tree-node.tsx b/packages/plugin-outline-pane/src/views/tree-node.tsx new file mode 100644 index 000000000..12300b4d8 --- /dev/null +++ b/packages/plugin-outline-pane/src/views/tree-node.tsx @@ -0,0 +1,58 @@ + + +export interface TreeNodeProps { + treeNode: TreeNode; +} + +@observer +export default class TreeNodeView extends Component<TreeNodeProps> { + shouldComponentUpdate() { + return false; + } + + render() { + const { treeNode } = this.props; + const className = classNames('tree-node', { + // 是否展开 + expanded: treeNode.expanded, + // 是否悬停 + hover: treeNode.hover, + // 是否选中 + selected: treeNode.selected, + // 是否隐藏 + hidden: treeNode.hidden, + // 是否忽略的 + ignored: treeNode.ignored, + // 是否锁定的 + locked: treeNode.locked, + // 是否投放响应 + dropping: treeNode.dropIndex != null, + // 是否? + highlight: treeNode.isDropContainer() && treeNode.dropIndex == null, + }); + + return ( + <div className={className} data-id={treeNode.id}> + <TreeTitle treeNode={treeNode} /> + <TreeBranches treeNode={treeNode} /> + </div> + ); + } +} + +export function findTargetByEvent(e: MouseEvent): HTMLElement | null { + return (e.target as HTMLElement).closest('.tree-node') as HTMLElement; +} + +export function getNodeIDFromTarget(target: HTMLElement): string | null { + return target.getAttribute('data-id'); +} + +export function getNodeIDByEvent(e: MouseEvent): string | null { + const target = findTargetByEvent(e); + if (target) { + return getNodeIDFromTarget(target); + } + + return null; +} diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx new file mode 100644 index 000000000..19125d7a6 --- /dev/null +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -0,0 +1,176 @@ +import { observer } from '@ali/recore'; +import React, { Component, KeyboardEvent } from 'react'; +import classNames from 'classnames'; +import ElementNode from '../../../../document/node/element-node'; +import { isElementNode } from '../../../../document/node'; +import { TreeNodeProps } from './tree-node'; +import TreeNodeIconView from './tree-node-icon-view'; +import CollapsedIcon from '../icons/caret-right.svg'; +import EyeCloseIcon from 'my-icons/eye-close.svg'; + +interface IState { + editing: boolean; +} + +@observer +export default class TreeNodeTitle extends Component<TreeNodeProps, IState> { + + private inputRef = React.createRef<HTMLInputElement>(); + + constructor(props: TreeNodeProps) { + super(props); + this.state = { + editing: false, + }; + } + + toggleIgnored() { + const treeNode = this.props.treeNode; + const node = treeNode.node as ElementNode; + if (treeNode.ignored) { + node.getDirective('x-ignore').remove(); + } else { + node.getDirective('x-ignore').value = true; + } + } + + toggleExpanded() { + const treeNode = this.props.treeNode; + const { expanded } = treeNode; + treeNode.expanded = !expanded; + } + + renderExpandIcon() { + const node = this.props.treeNode; + + if (!node.expandable) { + return null; + } + + return ( + <div + className="tree-node-collapsed-icon" + onClick={e => { + e.stopPropagation(); + this.toggleExpanded(); + }} + > + <CollapsedIcon /> + </div> + ); + } + + setTitle(xtitle: string = '') { + const { treeNode } = this.props; + const node = treeNode.node as ElementNode; + const title = node.getProp('x-title'); + if (xtitle && xtitle !== node.tagName) { + title.code = `"${xtitle}"`; + } else { + title.remove(); + } + } + + enableEdit = () => { + this.setState({ + editing: true, + }); + } + + cancelEdit() { + this.setState({ + editing: false, + }); + } + + saveEdit = () => { + const { current } = this.inputRef; + + if (current) { + this.setTitle(current.value); + } + + this.cancelEdit(); + } + + handleKeyUp(e: KeyboardEvent<HTMLInputElement>) { + if (e.keyCode === 13) { + this.saveEdit(); + } + if (e.keyCode === 27) { + this.cancelEdit(); + } + } + + componentDidUpdate() { + const { current } = this.inputRef; + if (current) { + current.select(); + } + } + + render() { + const { treeNode } = this.props; + const { editing } = this.state; + const { title } = treeNode; + const depth = treeNode.depth; + const indent = depth * 12; + + const titleClassName = classNames('tree-node-title'); + const titleTextClassName = classNames('tree-node-title-text', { + 'x-if-text': treeNode.hasXIf(), + 'x-for-text': treeNode.hasXFor(), + }); + const xForValue = treeNode.xForValue; + + return ( + <div + className={titleClassName} + ref={ref => treeNode.mount(ref)} + style={{ paddingLeft: indent, marginLeft: -indent }} + > + {this.renderExpandIcon()} + <div className="tree-node-icon"> + <TreeNodeIconView tagName={treeNode.node.tagName} /> + </div> + <div className="tree-node-title-inner" onDoubleClick={this.enableEdit}> + { + editing ? + <input + className="tree-node-title-input" + defaultValue={title.label} + onBlur={this.saveEdit} + onKeyUp={e => {this.handleKeyUp(e)}} + ref={this.inputRef} + /> + : + <div className={titleTextClassName}> + {title.label} + {xForValue && ( + <span className="info"> + (x <b>{xForValue.length}</b>) + </span> + )} + {treeNode.hasXIf() && ( + <span className="info"> + <b>{treeNode.flowHidden ? '' : '(visible)'}</b> + </span> + )} + </div> + } + + </div> + <div className="tree-node-ignored-icon"> + {isElementNode(treeNode.node) && !editing && ( + <EyeCloseIcon + onClick={(e: MouseEvent) => { + e.stopPropagation(); + this.toggleIgnored(); + }} + /> + )} + </div> + </div> + ); + } +} diff --git a/packages/plugin-outline-pane/src/views/tree.tsx b/packages/plugin-outline-pane/src/views/tree.tsx new file mode 100644 index 000000000..551147ea9 --- /dev/null +++ b/packages/plugin-outline-pane/src/views/tree.tsx @@ -0,0 +1,81 @@ +@observer +export default class TreeView extends React.Component { + private ref = React.createRef<HTMLDivElement>(); + private dispose?: () => void; + + hover(e: any) { + const treeNode = tree.getTreeNodeByEvent(e); + + if (!treeNode) { + return; + } + + edging.watch(treeNode.node); + } + + onClick(e: any) { + if (this.dragEvent && (this.dragEvent as any).shaken) { + return; + } + + const isMulti = e.metaKey || e.ctrlKey; + + const treeNode = tree.getTreeNodeByEvent(e); + + if (!treeNode) { + return; + } + + treeNode.select(isMulti); + + // 通知主画板滚动到对应位置 + activeTracker.track(treeNode.node); + } + + onMouseOver(e: any) { + if (dragon.dragging) { + return; + } + + this.hover(e); + } + + onMouseUp(e: any) { + if (dragon.dragging) { + return; + } + + this.hover(e); + } + + onMouseLeave() { + edging.watch(null); + } + + componentDidMount(): void { + if (this.ref.current) { + dragon.from(this.ref.current, (e: MouseEvent) => { + this.dragEvent = e; + + const treeNode = tree.getTreeNodeByEvent(e); + if (treeNode) { + return { + type: DragTargetType.Nodes, + nodes: [treeNode.node], + }; + } + return null; + }); + } + } + + render() { + const { tree } = this.props; + const root = tree.root; + return ( + <div className="my-outline-tree"> + <TreeNodeView key={root.id} treeNode={root} /> + </div> + ); + } +} diff --git a/packages/plugin-outline-pane/tsconfig.json b/packages/plugin-outline-pane/tsconfig.json new file mode 100644 index 000000000..aad669598 --- /dev/null +++ b/packages/plugin-outline-pane/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@recore/config/tsconfig", + "compilerOptions": { + "experimentalDecorators": true + }, + "include": [ + "./src/" + ] +} diff --git a/packages/plugin-setters/.eslintignore b/packages/plugin-setters/.eslintignore new file mode 100644 index 000000000..1fb2edf7c --- /dev/null +++ b/packages/plugin-setters/.eslintignore @@ -0,0 +1,6 @@ +.idea/ +.vscode/ +build/ +.* +~* +node_modules diff --git a/packages/plugin-setters/.eslintrc b/packages/plugin-setters/.eslintrc new file mode 100644 index 000000000..db78d35d1 --- /dev/null +++ b/packages/plugin-setters/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/@recore/config/.eslintrc" +} diff --git a/packages/plugin-setters/.prettierrc b/packages/plugin-setters/.prettierrc new file mode 100644 index 000000000..8748c5ed3 --- /dev/null +++ b/packages/plugin-setters/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/packages/plugin-setters/package.json b/packages/plugin-setters/package.json index 05ce25bac..5b11b4045 100644 --- a/packages/plugin-setters/package.json +++ b/packages/plugin-setters/package.json @@ -1,5 +1,21 @@ { + "name": "@ali/lowcode-plugin-setters", + "version": "0.0.0", + "description": "xxx for Ali lowCode engine", + "main": "src/index.tsx", + "files": [ + "lib" + ], + "scripts": { + "build": "tsc", + "test": "ava", + "test:snapshot": "ava --update-snapshots" + }, "dependencies": { + "@alifd/next": "^1.19.16", + "classnames": "^2.2.6", + "react": "^16", + "react-dom": "^16.7.0", "@ali/iceluna-comp-expression": "^1.0.6", "@ali/iceluna-comp-form": "^1.0.20", "@ali/iceluna-comp-list": "^1.0.26", @@ -7,12 +23,32 @@ "@ali/iceluna-comp-object-button": "^1.0.23", "@ali/iceluna-comp-react-node": "^1.0.5", "@ali/iceluna-sdk": "^1.0.5-beta.24", - "@alifd/next": "^1.19.16", - "@alife/next": "^1.19.16", "acorn": "^6.4.1", "intl-messageformat": "^8.2.1", "monaco-editor": "^0.20.0", "qs": "^6.9.1", "react-monaco-editor": "^0.34.0" + }, + "devDependencies": { + "@recore/config": "^2.0.0", + "@types/classnames": "^2.2.7", + "@types/node": "^13.7.1", + "@types/react": "^16", + "@types/react-dom": "^16", + "eslint": "^6.5.1", + "prettier": "^1.18.2", + "tslib": "^1.9.3", + "typescript": "^3.1.3", + "ts-node": "^8.0.1" + }, + "ava": { + "compileEnhancements": false, + "snapshotDir": "test/fixtures/__snapshots__", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] } } diff --git a/packages/editor/src/config/setters.ts b/packages/plugin-setters/src/index.tsx similarity index 54% rename from packages/editor/src/config/setters.ts rename to packages/plugin-setters/src/index.tsx index 32a404a41..3806f59b0 100644 --- a/packages/editor/src/config/setters.ts +++ b/packages/plugin-setters/src/index.tsx @@ -1,22 +1,25 @@ -import { DatePicker, Input, Radio, Select, Switch, NumberPicker } from '@alifd/next'; -import ExpressionSetter from '../../../plugin-setters/src/expression-setter'; -import MixinSetter from '../../../plugin-setters/src/mixin-setter'; -import EventsSetter from '../../../plugin-setters/src/events-setter'; -import { registerSetter } from '../../../plugin-settings/src'; -import { createElement } from 'react'; +import { registerSetter } from '../../globals/src'; +import { + DatePicker, + Input, + Radio, + Select, + Switch, + NumberPicker, +} from '@alifd/next'; +import ExpressionSetter from './expression-setter'; +import MixinSetter from './mixin-setter'; +import EventsSetter from './events-setter'; registerSetter('ClassNameSetter', () => { - return createElement( - 'div', - { - className: 'lc-block-setter' - }, - '这里是类名绑定' - ); + return <div className="lc-block-setter">这里是类名绑定</div>; }); registerSetter('EventsSetter', EventsSetter); -registerSetter('StringSetter', { component: Input, defaultProps: { placeholder: '请输入' } }); +registerSetter('StringSetter', { + component: Input, + defaultProps: { placeholder: '请输入' }, +}); registerSetter('NumberSetter', NumberPicker); registerSetter('ExpressionSetter', ExpressionSetter); registerSetter('MixinSetter', MixinSetter); @@ -24,8 +27,8 @@ registerSetter('BoolSetter', Switch); registerSetter('RadioGroupSetter', { component: Radio.Group, defaultProps: { - shape: 'button' - } + shape: 'button', + }, }); registerSetter('SelectSetter', Select); diff --git a/packages/plugin-setters/tsconfig.json b/packages/plugin-setters/tsconfig.json new file mode 100644 index 000000000..aad669598 --- /dev/null +++ b/packages/plugin-setters/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./node_modules/@recore/config/tsconfig", + "compilerOptions": { + "experimentalDecorators": true + }, + "include": [ + "./src/" + ] +} diff --git a/packages/plugin-settings/src/index.tsx b/packages/plugin-settings/src/index.tsx index 8ed11d15b..5f05461b2 100644 --- a/packages/plugin-settings/src/index.tsx +++ b/packages/plugin-settings/src/index.tsx @@ -1,13 +1,12 @@ import React, { Component } from 'react'; -import { Tab, Breadcrumb, Icon } from '@alifd/next'; +import { Tab, Breadcrumb } from '@alifd/next'; import { SettingsMain, SettingField, isSettingField } from './main'; import './style.less'; -import { Title, TipContainer } from '../../globals'; -import SettingsPane, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-pane'; +import { Title, TipContainer, createIcon } from '../../globals'; +import SettingsPane, { createSettingFieldView } from './settings-pane'; import Node from '../../designer/src/designer/document/node/node'; -import ArraySetter from './builtin-setters/array-setter'; -import ObjectSetter from './builtin-setters/object-setter'; -import './register-transducer'; +import './transducers/register'; +import './setters/register'; export default class SettingsMainView extends Component { private main: SettingsMain; @@ -32,10 +31,9 @@ export default class SettingsMainView extends Component { if (this.main.isMulti) { return ( <div className="lc-settings-navigator"> - {this.main.componentMeta!.icon || <Icon type="ellipsis" size="small" />} - <span> - {this.main.componentMeta!.title} x {this.main.nodes.length} - </span> + {createIcon(this.main.componentMeta?.icon)} + <Title title={this.main.componentMeta!.title} /> + <span>x {this.main.nodes.length}</span> </div> ); } @@ -52,13 +50,13 @@ export default class SettingsMainView extends Component { onMouseOut: hoverNode.bind(null, node, false), onClick: selectNode.bind(null, node), }; - items.unshift(<Breadcrumb.Item {...props} key={node.id}>{node.title}</Breadcrumb.Item>); + items.unshift(<Breadcrumb.Item {...props} key={node.id}><Title title={node.title} /></Breadcrumb.Item>); node = node.parent; } return ( <div className="lc-settings-navigator"> - {this.main.componentMeta!.icon || <Icon type="ellipsis" size="small" />} + {createIcon(this.main.componentMeta?.icon)} <Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb> </div> ); @@ -105,6 +103,7 @@ export default class SettingsMainView extends Component { <Tab navClassName="lc-settings-tabs" animation={false} + excessMode="dropdown" contentClassName="lc-settings-tabs-content" extra={this.renderBreadcrumb()} > @@ -126,7 +125,4 @@ function selectNode(node: Node) { node.select(); } -registerSetter('ArraySetter', ArraySetter); -registerSetter('ObjectSetter', ObjectSetter); - -export { registerSetter, createSetterContent, getSetter, createSettingFieldView }; +export { createSettingFieldView }; diff --git a/packages/plugin-settings/src/main.ts b/packages/plugin-settings/src/main.ts index 2cd28bbfc..f08a70370 100644 --- a/packages/plugin-settings/src/main.ts +++ b/packages/plugin-settings/src/main.ts @@ -2,9 +2,7 @@ import { EventEmitter } from 'events'; import { uniqueId } from '../../utils/unique-id'; import { ComponentMeta } from '../../designer/src/designer/component-meta'; import Node from '../../designer/src/designer/document/node/node'; -import { TitleContent } from '../../globals'; -import { ReactElement, ComponentType as ReactComponentType, isValidElement } from 'react'; -import { isReactComponent } from '../../utils/is-react'; +import { TitleContent, FieldExtraProps, SetterType, CustomView, FieldConfig, isCustomView } from '../../globals'; import Designer from '../../designer/src/designer/designer'; import { Selection } from '../../designer/src/designer/document/selection'; @@ -61,99 +59,6 @@ export interface SettingTarget { // 设置附属属性值 setExtraPropValue(propName: string, value: any): void; - - /* - // 所有属性值数据 - readonly props: object; - // 设置多个属性值,替换原有值 - setProps(data: object): void; - // 设置多个属性值,和原有值合并 - mergeProps(data: object): void; - // 绑定属性值发生变化时 - onPropsChange(fn: () => void): () => void; - */ -} - -export type CustomView = ReactElement | ReactComponentType<any>; - -export function isCustomView(obj: any): obj is CustomView { - return obj && (isValidElement(obj) || isReactComponent(obj)); -} - -export type DynamicProps = (field: SettingField) => object; - -export interface SetterConfig { - /** - * if *string* passed must be a registered Setter Name - */ - componentName: string | CustomView; - /** - * the props pass to Setter Component - */ - props?: object | DynamicProps; - children?: any; - isRequired?: boolean; - initialValue?: any | ((field: SettingField) => any); -} - -/** - * if *string* passed must be a registered Setter Name, future support blockSchema - */ -export type SetterType = SetterConfig | string | CustomView; - -export interface FieldExtraProps { - /** - * 是否必填参数 - */ - isRequired?: boolean; - /** - * default value of target prop for setter use - */ - defaultValue?: any; - getValue?: (field: SettingField, fieldValue: any) => any; - setValue?: (field: SettingField, value: any) => void; - /** - * the field conditional show, is not set always true - * @default undefined - */ - condition?: (field: SettingField) => boolean; - /** - * default collapsed when display accordion - */ - defaultCollapsed?: boolean; - /** - * important field - */ - important?: boolean; - /** - * internal use - */ - forceInline?: number; -} - -export interface FieldConfig extends FieldExtraProps { - type?: 'field' | 'group'; - /** - * the name of this setting field, which used in quickEditor - */ - name: string | number; - /** - * the field title - * @default sameas .name - */ - title?: TitleContent; - /** - * the field body contains when .type = 'field' - */ - setter?: SetterType; - /** - * the setting items which group body contains when .type = 'group' - */ - items?: FieldConfig[]; - /** - * extra props for field - */ - extraProps?: FieldExtraProps; } export class SettingField implements SettingTarget { diff --git a/packages/plugin-settings/src/register-transducer.ts b/packages/plugin-settings/src/register-transducer.ts deleted file mode 100644 index 5c1ba15d7..000000000 --- a/packages/plugin-settings/src/register-transducer.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { - PropConfig, - PropType, - Shape, - OneOf, - ObjectOf, - ArrayOf, - OneOfType, -} from '../../designer/src/designer/prop-config'; -import { SetterType, FieldConfig, SettingField } from './main'; -import { registerMetadataTransducer } from '../../designer/src/designer/component-meta'; - -export function propConfigToFieldConfig(propConfig: PropConfig): FieldConfig { - const { name, description } = propConfig; - const title = { - label: description?.slice(0, 10) || name, - tip: description ? `${name} | ${description}` : undefined, - }; - return { - title, - ...propConfig, - setter: propTypeToSetter(propConfig.propType), - }; -} - -export 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]*$/; - -// parseProps -registerMetadataTransducer(metadata => { - const { configure } = metadata; - - if (!metadata.props) { - return { - ...metadata, - configure: { - ...configure, - props: [], - }, - }; - } - const { props = [], component = {}, events = {}, styles = {} } = configure; - const supportedEvents: any[] | null = (events as any).supportedEvents ? null : []; - - 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, - }, - }; -}); - -// addon/platform custom -registerMetadataTransducer(metadata => { - const { componentName, configure = {} } = metadata; - if (componentName === 'Leaf') { - return { - ...metadata, - configure: { - ...configure, - combined: [ - { - name: 'children', - title: '内容设置', - 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: '通用', - 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 = [ - { - title: '属性', - name: '#props', - items: propsGroup, - }, - ]; - const stylesGroup = []; - if (styles?.supportClassName) { - stylesGroup.push({ - name: 'className', - title: '类名绑定', - setter: 'ClassNameSetter', - }); - } - if (styles?.supportInlineStyle) { - stylesGroup.push({ - name: 'style', - title: '行内样式', - setter: 'StyleSetter', - }); - } - if (stylesGroup.length > 0) { - combined.push({ - name: '#styles', - title: '样式', - items: stylesGroup, - }); - } - - if (eventsDefinition.length > 0) { - combined.push({ - name: '#events', - title: '事件', - items: [ - { - name: '!events', - title: '事件设置', - 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: '高级', - items: [], - }); - } else { - combined.push({ - name: '#advanced', - title: '高级', - items: [ - { - name: '__condition', - title: '条件显示', - setter: 'ExpressionSetter', - }, - { - name: '#loop', - title: '循环', - items: [ - { - name: '__loop', - title: '循环数据', - setter: { - componentName: 'MixinSetter', - props: { - // TODO: - setters: [ - { - componentName: 'JSONSetter', - props: { - mode: 'popup', - placeholder: '编辑数据', - }, - }, - { - componentName: 'ExpressionSetter', - props: { - placeholder: '绑定数据', - }, - }, - ], - }, - }, - }, - { - name: '__loopArgs.0', - title: '迭代变量名', - setter: { - componentName: 'StringSetter', - placeholder: '默认为 item', - }, - }, - { - name: '__loopArgs.1', - title: '索引变量名', - setter: { - componentName: 'StringSetter', - placeholder: '默认为 index', - }, - }, - { - name: 'key', - title: 'Key', - setter: 'ExpressionSetter', - }, - ], - }, - ], - }); - } - - return { - ...metadata, - configure: { - ...configure, - combined, - }, - }; -}); diff --git a/packages/plugin-settings/src/builtin-setters/array-setter/index.tsx b/packages/plugin-settings/src/setters/array-setter/index.tsx similarity index 100% rename from packages/plugin-settings/src/builtin-setters/array-setter/index.tsx rename to packages/plugin-settings/src/setters/array-setter/index.tsx diff --git a/packages/plugin-settings/src/builtin-setters/array-setter/sortable.less b/packages/plugin-settings/src/setters/array-setter/sortable.less similarity index 100% rename from packages/plugin-settings/src/builtin-setters/array-setter/sortable.less rename to packages/plugin-settings/src/setters/array-setter/sortable.less diff --git a/packages/plugin-settings/src/builtin-setters/array-setter/sortable.tsx b/packages/plugin-settings/src/setters/array-setter/sortable.tsx similarity index 100% rename from packages/plugin-settings/src/builtin-setters/array-setter/sortable.tsx rename to packages/plugin-settings/src/setters/array-setter/sortable.tsx diff --git a/packages/plugin-settings/src/builtin-setters/array-setter/style.less b/packages/plugin-settings/src/setters/array-setter/style.less similarity index 100% rename from packages/plugin-settings/src/builtin-setters/array-setter/style.less rename to packages/plugin-settings/src/setters/array-setter/style.less diff --git a/packages/plugin-settings/src/builtin-setters/object-setter/index.tsx b/packages/plugin-settings/src/setters/object-setter/index.tsx similarity index 100% rename from packages/plugin-settings/src/builtin-setters/object-setter/index.tsx rename to packages/plugin-settings/src/setters/object-setter/index.tsx diff --git a/packages/plugin-settings/src/builtin-setters/object-setter/style.less b/packages/plugin-settings/src/setters/object-setter/style.less similarity index 100% rename from packages/plugin-settings/src/builtin-setters/object-setter/style.less rename to packages/plugin-settings/src/setters/object-setter/style.less diff --git a/packages/plugin-settings/src/setters/register.ts b/packages/plugin-settings/src/setters/register.ts new file mode 100644 index 000000000..08b246c58 --- /dev/null +++ b/packages/plugin-settings/src/setters/register.ts @@ -0,0 +1,6 @@ +import ArraySetter from './array-setter'; +import ObjectSetter from './object-setter'; +import { registerSetter } from '../../../globals'; + +registerSetter('ArraySetter', ArraySetter); +registerSetter('ObjectSetter', ObjectSetter); diff --git a/packages/plugin-settings/src/settings-pane.tsx b/packages/plugin-settings/src/settings-pane.tsx index 710210597..c721aa850 100644 --- a/packages/plugin-settings/src/settings-pane.tsx +++ b/packages/plugin-settings/src/settings-pane.tsx @@ -1,62 +1,15 @@ -import { Component, ReactNode } from 'react'; +import { Component } from 'react'; import { createContent } from '../../utils/create-content'; import { shallowEqual } from '../../utils/shallow-equal'; import { SettingField, - CustomView, isSettingField, SettingTarget, - SetterConfig, - isCustomView, - DynamicProps, } from './main'; import { Field, FieldGroup } from './field'; -import { TitleContent } from '../../globals'; +import { CustomView, DynamicProps, intl, shallowIntl, isSetterConfig, createSetterContent } from '../../globals'; import PopupService from './popup'; -export type RegisteredSetter = { - component: CustomView; - defaultProps?: object; - title?: TitleContent; -}; - -const settersMap = new Map<string, RegisteredSetter>(); -export function registerSetter(type: string, setter: CustomView | RegisteredSetter) { - if (isCustomView(setter)) { - setter = { - component: setter, - title: (setter as any).displayName || (setter as any).name || 'CustomSetter' - }; - } - settersMap.set(type, setter); -} - -export function getSetter(type: string): RegisteredSetter | null { - return settersMap.get(type) || null; -} - -export function createSetterContent(setter: any, props: object): ReactNode { - if (typeof setter === 'string') { - setter = getSetter(setter); - if (!setter) { - return null; - } - if (setter.defaultProps) { - props = { - ...setter.defaultProps, - ...props, - }; - } - setter = setter.component; - } - - return createContent(setter, props); -} - -function isSetterConfig(obj: any): obj is SetterConfig { - return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj); -} - class SettingFieldView extends Component<{ field: SettingField }> { state = { visible: false, @@ -98,7 +51,11 @@ class SettingFieldView extends Component<{ field: SettingField }> { state.value = null; state.setterProps.multiValue = true; if (!('placeholder' in props)) { - state.setterProps.placeholder = '多种值'; + state.setterProps.placeholder = intl({ + type: 'i18n', + 'zh-CN': '多种值', + 'en-US': 'Multiple Value' + }); } } // TODO: error handling @@ -142,7 +99,8 @@ class SettingFieldView extends Component<{ field: SettingField }> { return ( <Field title={extraProps.forceInline ? null : title}> {createSetterContent(this.setterType, { - ...setterProps, + // TODO: refresh intl + ...shallowIntl(setterProps), forceInline: extraProps.forceInline, key: field.id, // === injection diff --git a/packages/plugin-settings/src/style.less b/packages/plugin-settings/src/style.less index 3255da07b..592dc3c9b 100644 --- a/packages/plugin-settings/src/style.less +++ b/packages/plugin-settings/src/style.less @@ -106,6 +106,7 @@ min-width: 0; .next-tabs-tab-inner { text-align: center; + padding: 12px 0; } } } diff --git a/packages/plugin-settings/src/transducers/addon-combine.ts b/packages/plugin-settings/src/transducers/addon-combine.ts new file mode 100644 index 000000000..b354a0f31 --- /dev/null +++ b/packages/plugin-settings/src/transducers/addon-combine.ts @@ -0,0 +1,253 @@ +import { TransformedComponentMetadata, FieldConfig } from '../../../globals/src'; +import { SettingField } from '../main'; + +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/plugin-settings/src/transducers/parse-props.ts b/packages/plugin-settings/src/transducers/parse-props.ts new file mode 100644 index 000000000..abf54d5bf --- /dev/null +++ b/packages/plugin-settings/src/transducers/parse-props.ts @@ -0,0 +1,222 @@ +import { FieldConfig, PropConfig, PropType, SetterType, OneOf, Shape, ObjectOf, ArrayOf, TransformedComponentMetadata } from '../../../globals/src'; + +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/plugin-settings/src/transducers/register.ts b/packages/plugin-settings/src/transducers/register.ts new file mode 100644 index 000000000..81c6ae10d --- /dev/null +++ b/packages/plugin-settings/src/transducers/register.ts @@ -0,0 +1,9 @@ +import { registerMetadataTransducer } from '../../../globals'; +import parseProps from './parse-props'; +import addonCombine from './addon-combine'; + +// parseProps +registerMetadataTransducer(parseProps); + +// addon/platform custom +registerMetadataTransducer(addonCombine); diff --git a/packages/protocal/README.md b/packages/protocal/README.md deleted file mode 100644 index 6eaa97500..000000000 --- a/packages/protocal/README.md +++ /dev/null @@ -1 +0,0 @@ -通用协议层