diff --git a/packages/demo/package.json b/packages/demo/package.json index 562476b97..ef36aa7dd 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -41,7 +41,8 @@ "@alife/theme-lowcode-light": "^0.1.0", "react": "^16.8.1", "react-dom": "^16.8.1", - "@ali/vu-function-parser": "^2.5.0-beta.0" + "@ali/vu-function-parser": "^2.5.0-beta.0", + "compare-versions": "^3.0.1" }, "devDependencies": { "@ali/iceluna-cli": "^0.0.16", diff --git a/packages/demo/src/vision/index.ts b/packages/demo/src/vision/index.ts index 157b496a3..e01b724c6 100644 --- a/packages/demo/src/vision/index.ts +++ b/packages/demo/src/vision/index.ts @@ -18,8 +18,11 @@ import { upgradeAssetsBundle } from './upgrade-assets'; import { isCSSUrl } from '@ali/lowcode-utils'; import { I18nSetter } from '@ali/visualengine-utils'; import VariableSetter from '@ali/vs-variable-setter'; -import { isObject, isArray } from 'lodash'; +import _isArray from "lodash/isArray"; +import _isObject from "lodash/isObject"; +import _get from 'lodash/get'; import funcParser from '@ali/vu-function-parser'; +import cv from 'compare-versions'; const { editor, skeleton, context, HOOKS, Trunk } = Engine; @@ -45,11 +48,7 @@ async function loadAssets() { if (assets.packages) { assets.packages.forEach((item: any) => { - if (item.package.indexOf('@ali/vc-') === 0 && item.urls) { - item.urls = item.urls.filter((url: string) => { - return url.indexOf('view.mobile') < 0; - }); - } else if (item.package && externals.indexOf(item.package) > -1) { + if (item.package && externals.indexOf(item.package) > -1) { item.urls = null; } }); @@ -103,7 +102,7 @@ function initDemoPanes() { props: { align: 'bottom', icon: 'set', - description: '设置', + description: '设置' }, }); skeleton.add({ @@ -113,7 +112,7 @@ function initDemoPanes() { props: { align: 'bottom', icon: 'help', - description: '帮助', + description: '帮助' }, }); @@ -336,9 +335,9 @@ function replaceFuncProp(props?: any){ } if ((prop.compiled && prop.source) || prop.type === 'actionRef' || prop.type === 'js') { replaceProps[name] = funcParser(prop); - } else if (isObject(prop)) { + } else if (_isObject(prop)) { replaceFuncProp(prop); - } else if (isArray(prop)) { + } else if (_isArray(prop)) { prop.map((propItem) => { replaceFuncProp(propItem); }); @@ -348,7 +347,6 @@ function replaceFuncProp(props?: any){ for (const name in replaceProps) { props[name] = replaceProps[name]; } - return props; }; @@ -399,7 +397,7 @@ function initHistoryPane() { historyManager: { historyManager, app: { - + } }, index: -940, diff --git a/packages/demo/src/vision/module.d.ts b/packages/demo/src/vision/module.d.ts index ba843aea9..be53328de 100644 --- a/packages/demo/src/vision/module.d.ts +++ b/packages/demo/src/vision/module.d.ts @@ -10,3 +10,4 @@ declare module '@ali/ve-i18n-manage-pane'; declare module '@ali/ve-action-pane'; declare module '@ali/vu-legao-design-fetch-context'; declare module "@ali/vu-function-parser"; +declare module "compare-versions"; diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx index 63201f31c..fa0d3b1dc 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-hovering.tsx @@ -39,27 +39,25 @@ export class BorderHoveringInstance extends PureComponent<{ } @observer -export class BorderHovering extends Component { - static contextType = SimulatorContext; - +export class BorderHovering extends Component<{ host: BuiltinSimulatorHost }> { shouldComponentUpdate() { return false; } @computed get scale() { - return (this.context as BuiltinSimulatorHost).viewport.scale; + return this.props.host.viewport.scale; } @computed get scrollX() { - return (this.context as BuiltinSimulatorHost).viewport.scrollX; + return this.props.host.viewport.scrollX; } @computed get scrollY() { - return (this.context as BuiltinSimulatorHost).viewport.scrollY; + return this.props.host.viewport.scrollY; } @computed get current() { - const host = this.context as BuiltinSimulatorHost; + const host = this.props.host; const doc = host.document; const selection = doc.selection; const current = host.designer.hovering.current; @@ -70,7 +68,7 @@ export class BorderHovering extends Component { } render() { - const host = this.context as BuiltinSimulatorHost; + const host = this.props.host; const current = this.current; if (!current || host.viewport.scrolling) { return ; diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx index 5597e9382..53b051ca5 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx @@ -132,11 +132,9 @@ function createAction(content: ReactNode | ComponentType | ActionContentObj } @observer -export class BorderSelectingForNode extends Component<{ node: Node }> { - static contextType = SimulatorContext; - +export class BorderSelectingForNode extends Component<{ host: BuiltinSimulatorHost; node: Node }> { get host(): BuiltinSimulatorHost { - return this.context; + return this.props.host; } get dragging(): boolean { @@ -177,11 +175,9 @@ export class BorderSelectingForNode extends Component<{ node: Node }> { } @observer -export class BorderSelecting extends Component { - static contextType = SimulatorContext; - +export class BorderSelecting extends Component<{ host: BuiltinSimulatorHost }> { get host(): BuiltinSimulatorHost { - return this.context; + return this.props.host; } get dragging(): boolean { @@ -211,7 +207,7 @@ export class BorderSelecting extends Component { return ( {selecting.map((node) => ( - + ))} ); diff --git a/packages/designer/src/builtin-simulator/bem-tools/borders.less b/packages/designer/src/builtin-simulator/bem-tools/borders.less index 93ed2c1db..af21cc622 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/borders.less +++ b/packages/designer/src/builtin-simulator/bem-tools/borders.less @@ -25,6 +25,9 @@ align-items: stretch; justify-content: flex-end; pointer-events: all; + > * { + flex-shrink: 0; + } } &-action { diff --git a/packages/designer/src/builtin-simulator/bem-tools/index.tsx b/packages/designer/src/builtin-simulator/bem-tools/index.tsx index 9235201b2..3b8d84788 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/index.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/index.tsx @@ -1,7 +1,6 @@ import { Component } from 'react'; import { observer } from '@ali/lowcode-editor-core'; import { BorderHovering } from './border-hovering'; -import { SimulatorContext } from '../context'; import { BuiltinSimulatorHost } from '../host'; import { BorderSelecting } from './border-selecting'; import BorderResizing from './border-resizing'; @@ -10,15 +9,13 @@ import './bem-tools.less'; import './borders.less'; @observer -export class BemTools extends Component { - static contextType = SimulatorContext; - +export class BemTools extends Component<{ host: BuiltinSimulatorHost }> { shouldComponentUpdate() { return false; } render() { - const host = this.context as BuiltinSimulatorHost; + const host = this.props.host; const { scrollX, scrollY, scale } = host.viewport; return (
diff --git a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx index f630e3547..f253d843b 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx @@ -112,24 +112,19 @@ function processDetail({ target, detail, document }: DropLocation): InsertionDat } @observer -export class InsertionView extends Component { - static contextType = SimulatorContext; - - @computed get host(): BuiltinSimulatorHost { - return this.context; - } - +export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> { shouldComponentUpdate() { return false; } render() { - const loc = this.host.document.dropLocation; + const { host } = this.props; + const loc = host.document.dropLocation; if (!loc) { return null; } - const { scale, scrollX, scrollY } = this.host.viewport; + const { scale, scrollX, scrollY } = host.viewport; const { edge, insertType, coverRect, nearRect, vertical } = processDetail(loc); if (!edge) { diff --git a/packages/designer/src/builtin-simulator/host-view.tsx b/packages/designer/src/builtin-simulator/host-view.tsx index 6d71ce9e3..23a5895ec 100644 --- a/packages/designer/src/builtin-simulator/host-view.tsx +++ b/packages/designer/src/builtin-simulator/host-view.tsx @@ -41,20 +41,17 @@ export class BuiltinSimulatorHostView extends Component { const { Provider } = SimulatorContext; return (
- - {/*progressing.visible ? : null*/} - - + {/*progressing.visible ? : null*/} +
); } } @observer -class Canvas extends Component { - static contextType = SimulatorContext; +class Canvas extends Component<{ host: BuiltinSimulatorHost }> { render() { - const sim = this.context as BuiltinSimulatorHost; + const sim = this.props.host; let className = 'lc-simulator-canvas'; if (sim.deviceClassName) { className += ` ${sim.deviceClassName}`; @@ -65,8 +62,8 @@ class Canvas extends Component { return (
sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport"> - - + +
); @@ -74,10 +71,9 @@ class Canvas extends Component { } @observer -class Content extends Component { - static contextType = SimulatorContext; +class Content extends Component<{ host: BuiltinSimulatorHost }> { render() { - const sim = this.context as BuiltinSimulatorHost; + const sim = this.props.host; const viewport = sim.viewport; let frameStyle = {}; if (viewport.scale < 1) { diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index 1954f6091..1410ea334 100644 --- a/packages/designer/src/component-meta.ts +++ b/packages/designer/src/component-meta.ts @@ -7,6 +7,8 @@ import { TitleContent, TransformedComponentMetadata, NestingFilter, + isTitleConfig, + I18nData, } from '@ali/lowcode-types'; import { computed } from '@ali/lowcode-editor-core'; import { Node, ParentalNode } from './document'; @@ -17,6 +19,8 @@ import { IconPage } from './icons/page'; import { IconComponent } from './icons/component'; import { IconRemove } from './icons/remove'; import { IconClone } from './icons/clone'; +import { ReactElement } from 'react'; +import { IconHidden } from './icons/hidden'; function ensureAList(list?: string | string[]): string[] | null { if (!list) { @@ -91,12 +95,20 @@ export class ComponentMeta { private childWhitelist?: NestingFilter | null; private _title?: TitleContent; - get title() { + get title(): string | I18nData | ReactElement { + // TODO: 标记下。这块需要康师傅加一下API,页面正常渲染。 + // string | i18nData | ReactElement + // TitleConfig title.label + if (isTitleConfig(this._title)) { + return (this._title.label as any) || this.componentName; + } return this._title || this.componentName; } @computed get icon() { + // TODO: 标记下。这块需要康师傅加一下API,页面正常渲染。 // give Slot default icon + // if _title is TitleConfig get _title.icon return ( this._transformedMetadata?.icon || (this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent) @@ -131,10 +143,10 @@ export class ComponentMeta { this._title = typeof title === 'string' ? { - type: 'i18n', - 'en-US': this.componentName, - 'zh-CN': title, - } + type: 'i18n', + 'en-US': this.componentName, + 'zh-CN': title, + } : title; } @@ -319,6 +331,20 @@ const builtinComponentActions: ComponentAction[] = [ }, important: true, }, + { + name: 'hide', + content: { + icon: IconHidden, + title: intlNode('hide'), + action(node: Node) { + node.getExtraProp('hidden', true)?.setValue(true); + }, + }, + condition: (node: Node) => { + return node.componentMeta.isModal; + }, + important: true, + }, { name: 'copy', content: { @@ -326,6 +352,8 @@ const builtinComponentActions: ComponentAction[] = [ title: intlNode('copy'), action(node: Node) { // node.remove(); + const { document: doc, parent, schema, index } = node; + parent && doc.insertNode(parent, schema, index); }, }, important: true, diff --git a/packages/designer/src/designer/builtin-hotkey.ts b/packages/designer/src/designer/builtin-hotkey.ts index d11edbe03..2babfb446 100644 --- a/packages/designer/src/designer/builtin-hotkey.ts +++ b/packages/designer/src/designer/builtin-hotkey.ts @@ -4,6 +4,67 @@ import { focusing } from './focusing'; import { insertChildren, TransformStage } from '../document'; import clipboard from './clipboard'; +function getNextForSelect(next: any, head?: any, parent?: any): any { + if (next) { + if (!head) { + return next; + } + + let ret; + if (next.isContainer()) { + const children = next.getChildren() || []; + if (children && !children.isEmpty()) { + ret = getNextForSelect(children.get(0)); + if (ret) { + return ret; + } + } + } + + ret = getNextForSelect(next.nextSibling); + if (ret) { + return ret; + } + } + + if (parent) { + return getNextForSelect(parent.nextSibling, false, parent.getParent()); + } + + return null; +} + +function getPrevForSelect(prev: any, head?: any, parent?: any): any { + if (prev) { + debugger; + let ret; + if (!head && prev.isContainer()) { + const children = prev.getChildren() || []; + const lastChild = children && !children.isEmpty() ? children.get(children.size - 1) : null; + + ret = getPrevForSelect(lastChild); + if (ret) { + return ret; + } + } + + if (!head) { + return prev; + } + + ret = getPrevForSelect(prev.prevSibling); + if (ret) { + return ret; + } + } + + if (parent) { + return parent; + } + + return null; +} + // hotkey binding hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => { const doc = focusing.focusDesigner?.currentDocument; @@ -58,14 +119,15 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => { const data = { type: 'nodeSchema', componentsMap, componentsTree }; clipboard.setData(data); - /* + const cutMode = action.indexOf('x') > 0; if (cutMode) { - const parentNode = selected.getParent(); - parentNode.select(); - selected.remove(); + selected.forEach((node) => { + const parentNode = node.getParent(); + parentNode?.select(); + node.remove(); + }); } - */ }); // command + v paste @@ -111,3 +173,92 @@ hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e) => { his.forward(); }); + +// sibling selection +hotkey.bind(['left', 'right'], (e, action) => { + const designer = focusing.focusDesigner; + const doc = designer?.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + const firstNode = selected[0]; + const silbing = action === 'left' ? firstNode?.prevSibling : firstNode?.nextSibling; + silbing?.select(); +}); + +hotkey.bind(['up', 'down'], (e, action) => { + const designer = focusing.focusDesigner; + const doc = designer?.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + const firstNode = selected[0]; + + if (action === 'down') { + const next = getNextForSelect(firstNode, true, firstNode.getParent()); + next?.select(); + } else if (action === 'up') { + const prev = getPrevForSelect(firstNode, true, firstNode.getParent()); + prev?.select(); + } +}); + +hotkey.bind(['option+up', 'option+down', 'option+left', 'option+right'], (e, action) => { + const designer = focusing.focusDesigner; + const doc = designer?.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 + + const firstNode = selected[0]; + const parent = firstNode.getParent(); + if (!parent) return; + + const isPrev = /(up|left)$/.test(action); + const isTravel = /(up|down)$/.test(action); + + const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling; + if (silbing) { + if (isTravel && silbing.isContainer()) { + const place = silbing.getSuitablePlace(firstNode, null); + if (isPrev) { + place.container.insertAfter(firstNode, place.ref); + } else { + place.container.insertBefore(firstNode, place.ref); + } + } else if (isPrev) { + parent.insertBefore(firstNode, silbing); + } else { + parent.insertAfter(firstNode, silbing); + } + firstNode?.select(); + return; + } + if (isTravel) { + const place = parent.getSuitablePlace(firstNode, null); // upwards + if (place) { + if (isPrev) { + place.container.insertBefore(firstNode, place.ref); + } else { + place.container.insertAfter(firstNode, place.ref); + } + firstNode?.select(); + } + } +}); diff --git a/packages/designer/src/designer/setting/setting-prop-entry.ts b/packages/designer/src/designer/setting/setting-prop-entry.ts index a231eaf2b..2e11f82b5 100644 --- a/packages/designer/src/designer/setting/setting-prop-entry.ts +++ b/packages/designer/src/designer/setting/setting-prop-entry.ts @@ -180,6 +180,11 @@ export class SettingPropEntry implements SettingEntry { return this.top; } + // add settingfield props + get props() { + return this.top; + } + onValueChange(func: () => any) { this.emitter.on('valuechange', func); diff --git a/packages/designer/src/designer/setting/utils.js b/packages/designer/src/designer/setting/utils.js index d9e7e389c..c49011bbf 100644 --- a/packages/designer/src/designer/setting/utils.js +++ b/packages/designer/src/designer/setting/utils.js @@ -1,6 +1,8 @@ // all this file for polyfill vision logic import { isValidElement } from 'react'; +import { isSetterConfig } from '@ali/lowcode-types'; +import { getSetter } from '@ali/lowcode-editor-core'; function getHotterFromSetter(setter) { return setter && (setter.Hotter || (setter.type && setter.type.Hotter)) || []; // eslint-disable-line @@ -29,9 +31,9 @@ export class Transducer { constructor(context, config) { let { setter } = config; - // 1. validElement + // 1. validElement // 2. SetterConfig - // 3. SetterConfig[] + // 3. SetterConfig[] if (Array.isArray(setter)) { setter = setter[0]; } else if (isValidElement(setter) && setter.type.displayName === 'MixedSetter') { @@ -40,6 +42,13 @@ export class Transducer { setter = setter.props.setters[0]; } + if (isSetterConfig(setter)) { + setter = setter.componentName; + } + if (typeof setter === 'string') { + setter = getSetter(setter); + } + this.setterTransducer = combineTransducer( getTransducerFromSetter(setter), getHotterFromSetter(setter), diff --git a/packages/designer/src/document/document-model.ts b/packages/designer/src/document/document-model.ts index ac739d625..e4442734f 100644 --- a/packages/designer/src/document/document-model.ts +++ b/packages/designer/src/document/document-model.ts @@ -41,6 +41,7 @@ export class DocumentModel { private seqId = 0; private _simulator?: ISimulatorHost; + /** * 模拟器 */ @@ -110,6 +111,14 @@ export class DocumentModel { } readonly designer = this.project.designer; + // getAddonData(name: string) { + // const addon = this.addons.find((item) => item.name === name); + // if (addon) { + // return addon.exportData(); + // } + // return this.addonsData[name]; + // } + /** * 生成唯一id @@ -230,6 +239,14 @@ export class DocumentModel { this.selection.remove(node.id); node.remove(); } + getAddonData(name: string) { + const addon = this.getNode(name) + if (addon) { + // 无法确定是否有这个api + // return addon.exportData(); + } + return addon + } @obx.ref private _dropLocation: DropLocation | null = null; /** @@ -461,14 +478,16 @@ export class DocumentModel { return config.checkNestingDown(parent, obj) && this.checkNestingUp(parent, obj); } - // ======= compatibles + // ======= compatibles for vision getRoot() { return this.rootNode; } - /** - * 兼容vision - */ + // add toData + toData() { + return { componentsTree: [this.project?.currentDocument?.export(TransformStage.Save)] }; + } + getHistory(): History { return this.history; } diff --git a/packages/designer/src/document/history.ts b/packages/designer/src/document/history.ts index 0f283b65b..8a37f1032 100644 --- a/packages/designer/src/document/history.ts +++ b/packages/designer/src/document/history.ts @@ -174,6 +174,10 @@ export class History { this.emitter.removeAllListeners(); this.records = []; } + + isModified() { + return this.point !== this.session.cursor; + } } class Session { diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index 4cc95b4e1..6239cfa60 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -7,6 +7,7 @@ import { PropsList, NodeData, TitleContent, + I18nData, SlotSchema, PageSchema, ComponentSchema, @@ -19,6 +20,7 @@ import { Prop } from './props/prop'; import { ComponentMeta } from '../../component-meta'; import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group'; import { TransformStage } from './transform-stage'; +import { ReactElement } from 'react'; /** * 基础节点 @@ -122,7 +124,7 @@ export class Node { return 0; } - @computed get title(): TitleContent { + @computed get title(): string | I18nData | ReactElement { let t = this.getExtraProp('title'); if (!t && this.componentMeta.descriptor) { t = this.getProp(this.componentMeta.descriptor, false); @@ -136,6 +138,10 @@ export class Node { return this.componentMeta.title; } + get icon() { + return this.componentMeta.icon; + } + constructor(readonly document: DocumentModel, nodeSchema: Schema) { const { componentName, id, children, props, ...extras } = nodeSchema; this.id = id || `node$${document.nextId()}`; @@ -154,8 +160,9 @@ export class Node { private transformProps(props: any): any { // FIXME! support PropsList - return this.document.designer.transformProps(props, this, TransformStage.Init); + const x = this.document.designer.transformProps(props, this, TransformStage.Init); + return x; // TODO: run transducers in metadata.experimental } @@ -177,19 +184,19 @@ export class Node { return this.isParental() && this.componentMeta.isContainer; } - isRoot(): this is RootNode { + isRoot(): boolean { return this.document.rootNode == (this as any); } - isPage(): this is PageNode { + isPage(): boolean { return this.isRoot() && this.componentName === 'Page'; } - isComponent(): this is ComponentNode { + isComponent(): boolean { return this.isRoot() && this.componentName === 'Component'; } - isSlot(): this is SlotNode { + isSlot(): boolean { return this._slotFor != null && this.componentName === 'Slot'; } @@ -219,8 +226,12 @@ export class Node { return; } - if (!this.isSlot() && this._parent) { - this._parent.children.delete(this); + if (this._parent) { + if (this.isSlot()) { + this._parent.removeSlot(this, false); + } else { + this._parent.children.delete(this); + } } this._parent = parent; @@ -252,8 +263,12 @@ export class Node { * 移除当前节点 */ remove() { - if (!this.isSlot() && this.parent) { - this.parent.children.delete(this, true); + if (this.parent) { + if (this.isSlot()) { + this.parent.removeSlot(this, true); + } else { + this.parent.children.delete(this, true); + } } } @@ -289,24 +304,13 @@ export class Node { return this.props.export(TransformStage.Serilize).props || null; } + @obx.val _slots: Node[] = []; @computed hasSlots() { - for (const item of this.props) { - if (item.type === 'slot') { - return true; - } - } - return false; + return this._slots.length > 0; } - @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; + get slots() { + return this._slots; } @obx.ref private _conditionGroup: ExclusiveGroup | null = null; @@ -352,7 +356,7 @@ export class Node { @computed hasCondition() { const v = this.getExtraProp('condition', false)?.getValue(); - return v != null && v !== ''; + return v != null && v !== '' && v !== true; } @computed hasLoop() { @@ -536,6 +540,28 @@ export class Node { return comparePosition(this, otherNode); } + /** + * 删除一个Slot节点 + */ + removeSlot(slotNode: Node, purge = false): boolean { + const i = this._slots.indexOf(slotNode); + if (i < 0) { + return false; + } + const deleted = this._slots.splice(i, 1)[0]; + if (purge) { + // should set parent null + deleted.internalSetParent(null); + deleted.purge(); + } + return false; + } + + addSlot(slotNode: Node) { + slotNode.internalSetParent(this as ParentalNode); + this._slots.push(slotNode); + } + private purged = false; /** * 是否已销毁 @@ -679,9 +705,20 @@ export class Node { } getRect(): DOMRect | null { + if (this.isRoot()) { + return this.document.simulator?.viewport.contentBounds || null; + } return this.document.simulator?.computeRect(this) || null; } + getPrototype() { + return this; + } + + getIcon() { + return this.icon; + } + toString() { return this.id; } diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index 39436aaec..da257b6bf 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -221,7 +221,7 @@ export class Prop implements IPropParent { } else { const owner = this.props.owner; this._slotNode = owner.document.createNode(slotSchema); - this._slotNode.internalSetParent(owner as any); + owner.addSlot(this._slotNode); this._slotNode.internalSetSlotFor(this); } this.dispose(); diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts index c9d61056a..98ac7a765 100644 --- a/packages/designer/src/document/node/props/props.ts +++ b/packages/designer/src/document/node/props/props.ts @@ -309,4 +309,11 @@ export class Props implements IPropParent { getPropValue(path: string): any { return this.getProp(path, false)?.value; } + + /** + * 设置单个属性值 + */ + setPropValue(path: string, value: any) { + this.getProp(path, true)!.setValue(value); + } } diff --git a/packages/designer/src/locale/en-US.json b/packages/designer/src/locale/en-US.json index 69eb61330..d895e4aba 100644 --- a/packages/designer/src/locale/en-US.json +++ b/packages/designer/src/locale/en-US.json @@ -1,6 +1,7 @@ { "copy": "Copy", "remove": "Remove", + "hide": "Hide", "Condition Group": "Condition Group", "No opened document": "No opened document, open some document to editing" } diff --git a/packages/designer/src/locale/zh-CN.json b/packages/designer/src/locale/zh-CN.json index cafba4f51..dc57f75ff 100644 --- a/packages/designer/src/locale/zh-CN.json +++ b/packages/designer/src/locale/zh-CN.json @@ -1,6 +1,7 @@ { "copy": "复制", "remove": "删除", + "hide": "隐藏", "Condition Group": "条件组", "No opened document": "没有打开的页面,请选择页面打开编辑" } diff --git a/packages/editor-core/src/intl/ali-global-locale.ts b/packages/editor-core/src/intl/ali-global-locale.ts index 0eef45403..1d4127649 100644 --- a/packages/editor-core/src/intl/ali-global-locale.ts +++ b/packages/editor-core/src/intl/ali-global-locale.ts @@ -49,13 +49,14 @@ class AliGlobalLocale { } } else if (g_config) { if (g_config.locale) { - return languageMap[g_config.locale] || (g_config.locale || '').replace('_', '-'); + return languageMap[g_config.locale] || g_config.locale.replace('_', '-'); } } let locale: string = ''; if (navigator.language) { - locale = (navigator.language as string).replace('_', '-'); + const lang = (navigator.language as string); + return languageMap[lang] || lang.replace('_', '-'); } // IE10 及更低版本使用 browserLanguage diff --git a/packages/editor-core/src/intl/index.ts b/packages/editor-core/src/intl/index.ts index f7319fb00..88974b764 100644 --- a/packages/editor-core/src/intl/index.ts +++ b/packages/editor-core/src/intl/index.ts @@ -37,10 +37,13 @@ function injectVars(msg: string, params: any, locale: string): string { });*/ } -export function intl(data: any, params?: object): string { +export function intl(data: any, params?: object): ReactNode { if (!isI18nData(data)) { return data; } + if (data.intl) { + return data.intl; + } const locale = globalLocale.getLocale(); const tries = generateTryLocales(locale); let msg: string | undefined; diff --git a/packages/editor-core/src/utils/focusing-track.ts b/packages/editor-core/src/utils/focusing-track.ts new file mode 100644 index 000000000..0f8ca6f84 --- /dev/null +++ b/packages/editor-core/src/utils/focusing-track.ts @@ -0,0 +1,48 @@ + +class FocusingManager { + deploy() { + + } + send(e: MouseEvent | KeyboardEvent) { + + } + addModalCheck() { + + } + create(config: FocusableConfig) { + + } + activeItem() { + + } + suspenceItem() { + + } +} + +export interface FocusableConfig { + range: HTMLElement | ((e: MouseEvent) => boolean); + modal?: boolean; + onEsc?: () => void; + onBlur?: () => void; +} + +class Focusable { + readonly isModal: boolean; + constructor(private manager: FocusingManager, { range, modal }: FocusableConfig) { + this.isModal = modal == null ? false : modal; + + } + checkRange(e: MouseEvent) { + + } + active() { + this.manager.activeItem(this); + } + suspence() { + this.manager.suspenceItem(this); + } + purge() { + + } +} diff --git a/packages/editor-skeleton/src/components/field/index.less b/packages/editor-skeleton/src/components/field/index.less index dd720a183..00e073111 100644 --- a/packages/editor-skeleton/src/components/field/index.less +++ b/packages/editor-skeleton/src/components/field/index.less @@ -13,7 +13,7 @@ align-items: center; } .lc-field-icon { - margin-right: @x-gap; + // margin-right: @x-gap; transform-origin: center; transition: transform 0.1s; } @@ -21,7 +21,7 @@ &.lc-plain-field { // for top-level style - padding: 8px 10px; + // padding: 8px 10px; > .lc-field-body { flex: 1; min-width: 0; @@ -34,7 +34,16 @@ display: flex; align-items: center; // for top-level style - padding: 8px 10px; + padding: 16px; + &:first-child{ + padding-top: 16px; + } + &:last-child{ + padding-bottom: 16px; + } + &+.lc-inline-field{ + padding-top: 0; + } > .lc-field-head { width: 70px; @@ -58,19 +67,29 @@ border-top: 1px solid var(--color-line-normal); } > .lc-field-head { - padding-left: @x-gap; + // padding-left: @x-gap; height: 32px; display: flex; align-items: center; font-weight: 500; - background: var(--color-block-background-shallow, rgba(31,56,88,.06)); - border-bottom: 1px solid var(--color-line-normal); - color: var(--color-title); + // background: var(--color-block-background-shallow, rgba(31,56,88,.06)); + // border-bottom: 1px solid var(--color-line-normal); + // color: var(--color-title); + padding: 0 16px; + background-color: #F7F9FC; + color: #8F9BB3; user-select: none; } > .lc-field-body { - padding: @y-gap @x-gap/2; + // padding: @y-gap @x-gap/2; + padding: 16px; + .lc-inline-field{ + margin-bottom: 16px; + &:last-child{ + margin-bottom: 0; + } + } } + .lc-inline-field { @@ -94,7 +113,11 @@ } &.lc-accordion-field { + position: relative; // collapsed + &:last-child.lc-field-is-collapsed{ + border-bottom: 1px solid var(--color-line-normal); + } &.lc-field-is-collapsed { > .lc-field-head .lc-field-icon { transform: rotate(180deg); @@ -106,14 +129,15 @@ // 邻近的保持上下距离 + .lc-field { - margin-top: @y-gap; + // margin-top: @y-gap; } } // 2rd level reset .lc-field-body { .lc-inline-field { - padding: @y-gap @x-gap/2 0 @x-gap/2; + // padding: @y-gap @x-gap/2 0 @x-gap/2; + padding: 0; &:first-child { padding-top: 0; } @@ -130,9 +154,10 @@ > .lc-field-head { padding-left: @x-gap/2; background: var(--color-block-background-light); - border-bottom-color: var(--color-line-light); + border-bottom-color: var(--color-line-light, rgba(31, 56, 88, .1)); > .lc-field-icon { - margin-right: @x-gap/2; + // margin-right: @x-gap/2; + margin-right: 0; } } } @@ -146,5 +171,8 @@ } } } + >.lc-block-setter { + flex: 1; + } } } diff --git a/packages/editor-skeleton/src/components/mixed-setter/style.less b/packages/editor-skeleton/src/components/mixed-setter/style.less index 5d6f31674..759fab709 100644 --- a/packages/editor-skeleton/src/components/mixed-setter/style.less +++ b/packages/editor-skeleton/src/components/mixed-setter/style.less @@ -42,9 +42,22 @@ margin-right: 0; >.lc-setter-actions { position: absolute; - right: 10px; + right: 16px; top: 0; height: 32px; transform: none; } } + +.lc-block-field > .lc-field-body > .lc-setter-mixed{ + +} +.lc-accordion-field > .lc-field-body > .lc-setter-mixed{ + position: static; + margin-right: 0; + > .lc-setter-actions{ + right: 32px; + top: 6px; + transform: none; + } +} diff --git a/packages/editor-skeleton/src/components/settings/main.ts b/packages/editor-skeleton/src/components/settings/main.ts index 8296ad7ef..14901aab5 100644 --- a/packages/editor-skeleton/src/components/settings/main.ts +++ b/packages/editor-skeleton/src/components/settings/main.ts @@ -70,13 +70,6 @@ export class SettingsMain { this._settings = this.designer.createSettingEntry(this.editor, nodes); } - onceOutlineVisible(fn: () => void): () => void { - this.emitter.on('outline-visible', fn); - return () => { - this.emitter.removeListener('outline-visible', fn); - }; - } - purge() { this.disposeListener(); this.emitter.removeAllListeners(); diff --git a/packages/editor-skeleton/src/components/settings/style.less b/packages/editor-skeleton/src/components/settings/style.less index 9b2c2b403..a76bfbc78 100644 --- a/packages/editor-skeleton/src/components/settings/style.less +++ b/packages/editor-skeleton/src/components/settings/style.less @@ -15,7 +15,7 @@ height: 30px; display: flex; align-items: center; - padding-left: 5px; + padding: 0 16px; border-bottom: 1px solid var(--color-line-normal); .lc-settings-navigator-icon { width: 16px; diff --git a/packages/plugin-outline-pane/src/icons/slot.tsx b/packages/editor-skeleton/src/icons/slot.tsx similarity index 100% rename from packages/plugin-outline-pane/src/icons/slot.tsx rename to packages/editor-skeleton/src/icons/slot.tsx diff --git a/packages/editor-skeleton/src/layouts/left-float-pane.tsx b/packages/editor-skeleton/src/layouts/left-float-pane.tsx index d9b32b22a..62c1bc3d5 100644 --- a/packages/editor-skeleton/src/layouts/left-float-pane.tsx +++ b/packages/editor-skeleton/src/layouts/left-float-pane.tsx @@ -12,6 +12,8 @@ export default class LeftFloatPane extends Component<{ area: Area }> } private dispose?: () => void; + // private focusing?: FocusingItem; + private shell: HTMLElement | null = null; componentDidMount() { const { area } = this.props; const triggerClose = () => area.setVisible(false); @@ -19,18 +21,44 @@ export default class LeftFloatPane extends Component<{ area: Area }> this.dispose = () => { area.skeleton.editor.removeListener('designer.dragstart', triggerClose); } + + /* + this.focusing = focusingTrack.create(this.shell!, { + onEsc: () => { + this.props.area.setVisible(false); + }, + onBlur: () => { + this.props.area.setVisible(false); + }, + // modal: boolean + }); + */ + + this.onEffect(); + } + + onEffect() { + /* + const { area } = this.props; + if (area.visible) { + this.focusing?.active(); + } else { + this.focusing?.suspense(); + } + */ + } + + componentDidUpdate() { + this.onEffect(); } componentWillUnmount() { + // this.focusing?.purge(); this.dispose?.(); } render() { const { area } = this.props; - // TODO: add focusingManager - // focusin set focus (push|replace) - // focusout remove focus - // onEsc const width = area.current?.config.props?.width; const hideTitleBar = area.current?.config.props?.hideTitleBar; const style = width ? { @@ -38,6 +66,7 @@ export default class LeftFloatPane extends Component<{ area: Area }> } : undefined; return (
{ this.shell = ref }} className={classNames('lc-left-float-pane', { 'lc-area-visible': area.visible, })} diff --git a/packages/editor-skeleton/src/layouts/main-area.tsx b/packages/editor-skeleton/src/layouts/main-area.tsx index 891053427..9361a320f 100644 --- a/packages/editor-skeleton/src/layouts/main-area.tsx +++ b/packages/editor-skeleton/src/layouts/main-area.tsx @@ -13,7 +13,7 @@ export default class MainArea extends Component<{ area: Area +
{area.container.items.map((item) => item.content)}
); diff --git a/packages/editor-skeleton/src/layouts/right-area.tsx b/packages/editor-skeleton/src/layouts/right-area.tsx index 9379f55b1..69178edc3 100644 --- a/packages/editor-skeleton/src/layouts/right-area.tsx +++ b/packages/editor-skeleton/src/layouts/right-area.tsx @@ -12,7 +12,7 @@ export default class RightArea extends Component<{ area: Area }> { render() { const { area } = this.props; return ( -
diff --git a/packages/editor-skeleton/src/layouts/top-area.tsx b/packages/editor-skeleton/src/layouts/top-area.tsx index 2b8535117..b9dee5c38 100644 --- a/packages/editor-skeleton/src/layouts/top-area.tsx +++ b/packages/editor-skeleton/src/layouts/top-area.tsx @@ -4,23 +4,23 @@ import { observer } from '@ali/lowcode-editor-core'; import Area from '../area'; @observer -export default class TopArea extends Component<{ area: Area }> { +export default class TopArea extends Component<{ area: Area, itemClassName?: string }> { render() { - const { area } = this.props; + const { area, itemClassName } = this.props; return ( -
- +
); } } @observer -class Contents extends Component<{ area: Area }> { +class Contents extends Component<{ area: Area, itemClassName?: string }> { render() { - const { area } = this.props; + const { area, itemClassName } = this.props; const left: any[] = []; const center: any[] = []; const right: any[] = []; @@ -29,12 +29,17 @@ class Contents extends Component<{ area: Area }> { const index2 = b.config?.index || 0; return index1 === index2 ? 0 : (index1 > index2 ? 1 : -1); }).forEach(item => { + const content = ( +
+ {item.content} +
+ ); if (item.align === 'center') { - center.push(item.content); + center.push(content); } else if (item.align === 'left') { - left.push(item.content); + left.push(content); } else { - right.push(item.content); + right.push(content); } }); return ( diff --git a/packages/editor-skeleton/src/layouts/workbench.less b/packages/editor-skeleton/src/layouts/workbench.less index 4efca072d..70a07c0ca 100644 --- a/packages/editor-skeleton/src/layouts/workbench.less +++ b/packages/editor-skeleton/src/layouts/workbench.less @@ -54,14 +54,11 @@ body { display: none; } .lc-panel-title { - height: 38px; - font-size: 14px; - background-color: var(--pane-title-bg-color,rgba(31,56,88,.04)); + // background-color: var(--pane-title-bg-color,rgba(31,56,88,.04)); display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; padding: 0 15px; - border-bottom: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); .lc-help-tip { margin-left: 4px; @@ -69,10 +66,18 @@ body { cursor: pointer; } } + > .lc-panel-title { + height: 48px; + font-size: 16px; + padding: 0 15px; + // border-bottom: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); + color: #0F1726; + font-weight: bold; + } .lc-panel-body { position: absolute; - top: 38px; + top: 48px; bottom: 0; left: 0; right: 0; @@ -111,6 +116,9 @@ body { } */ } + .lc-outline-pane{ + border-top: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); + } } .lc-panel { height: 100%; @@ -153,23 +161,88 @@ body { } } */ -} + /*覆盖旧面板*/ + /*组件面板*/ + // .ve-component-list { + // .ve-component-list-body{ + // .ve-component-list-sidebar{ + // .ve-component-list-navigator{ + // .navigator-group{ + // &:last-child{ + // &::after{ + // display: none; + // } + // } + // &::after{ + // content: ''; + // display: block; + // height: 1px; + // background-color: #EDEFF3; + // line-height: 0; + // margin: 4px 12px 0; + // } + // .navigator-group-head{ + // .navigator-group-title{ + // border-bottom: none; + // } + // } + // .navigator-group-item{ + // border-left: 2px solid transparent; + // &.active{ + // border-left-color: #0079f2; + // border-right: none; + // } + // } + // } + // } + // } + // } + // } + /*数据源*/ + // .engine-datapool{ + // .engine-datapool-view-group{ + // padding-top: 48px; + // .engine-datapool-view-group-title{ + // height: 48px; + // line-height: 48px; + // font-size: 16px; + // background-color: transparent; + // padding: 0 16px; + // border-bottom: 1px solid #EDEFF3; + // } + // } + // } + /*动作面板*/ + // .ve-action-pane{ + // border-top: none; + // .rc-tabs{ + // .rc-tabs-bar{ + // background-color: transparent; + // .rc-tabs-tab{ + // line-height: 1; + // &.rc-tabs-tab-active{ -.my-dock { - padding: 0px 10px; - cursor: pointer; - align-self: stretch; - display: flex; - align-items: center; - .my-title-label { - user-select: none; - } - &.actived, &:hover { - background-color: var(--pane-title-bg-color); - .my-title { - color: var(--color-actived); - } - } + // } + // } + // } + // } + // } + + /*设置面板*/ + // .ve-field .ve-field-head, + // .ve-field.ve-accordion2-field > .ve-field-head .ve-field-title-content{ + // padding: 0; + // } + // .ve-field.ve-accordion2-field > .ve-field-split-line{ + // display: none; + // } + // .vs-style .vs-style-source{ + // margin: 0 0 16px; + // } + // .vs-code-button, + // .vs-json-button{ + // margin: 0; + // } } @@ -184,13 +257,13 @@ body { width: 100%; display: flex; margin-bottom: 2px; - padding: 8px; - .lc-top-area-left{} + padding: 8px 12px 8px 16px; + .lc-top-area-center{ flex: 1; display: flex; - justify-content: flex-end; - margin-right: 8px; + justify-content: center; + margin: 0 8px; } .lc-top-area-right{ display: flex; @@ -222,8 +295,9 @@ body { } .lc-pane-close{ position: absolute; - right: 10px; - top: 6px; + right: 16px; + top: 16px; + height: auto; z-index: 2; .next-icon{ line-height: 1; @@ -231,17 +305,32 @@ body { } .lc-tabs-title { width: 100%; - height: 36px; + height: 32px; position: relative; display: center; display: flex; justify-content: center; align-items: center; - background: rgba(31,56,88,0.04); + // background: rgba(31,56,88,0.04); + border-bottom: 1px solid #EDEFF3; + .lc-tab-title{ + flex: 1; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 2px solid transparent; + cursor: pointer; + font-size: 12px; + &.actived { + color: #0079F2; + border-bottom-color: #0079F2; + } + } } .lc-tabs-content { position: absolute; - top: 36px; + top: 32px; bottom: 0; left: 0; right: 0; @@ -265,16 +354,23 @@ body { flex-direction: column; justify-content: flex-start; align-items: center; - .lc-title{ - padding: 12px; + .lc-title { flex-direction: column; - &.has-tip{ + width: 46px; + height: 46px; + display: flex; + align-items: center; + justify-content: center; + + &.has-tip { cursor: pointer; } &.actived{ color: #0079F2; } - .lc-title-icon{ + .lc-title-icon { + height: 20px; + width: 20px; margin: 0; .next-icon:before { line-height: 1 !important; @@ -301,8 +397,9 @@ body { } .lc-pane-close { position: absolute; - right: 10px; - top: 6px; + right: 16px; + top: 16px; + height: auto; z-index: 2; .next-icon { line-height: 1; @@ -324,6 +421,7 @@ body { flex: 1; display: flex; flex-direction: column; + z-index: 10; .lc-toolbar { height: var(--toolbar-height); background-color: var(--color-pane-background); diff --git a/packages/editor-skeleton/src/layouts/workbench.tsx b/packages/editor-skeleton/src/layouts/workbench.tsx index af50045c4..06f0b5055 100644 --- a/packages/editor-skeleton/src/layouts/workbench.tsx +++ b/packages/editor-skeleton/src/layouts/workbench.tsx @@ -13,16 +13,16 @@ import RightArea from './right-area'; import './workbench.less'; @observer -export class Workbench extends Component<{ skeleton: Skeleton, className?: string }> { +export class Workbench extends Component<{ skeleton: Skeleton, className?: string, topAreaItemClassName?: string }> { shouldComponentUpdate() { return false; } render() { - const { skeleton, className } = this.props; + const { skeleton, className, topAreaItemClassName } = this.props; return (
- +
diff --git a/packages/editor-skeleton/src/transducers/addon-combine.ts b/packages/editor-skeleton/src/transducers/addon-combine.ts index c0ee2a480..6c4015d14 100644 --- a/packages/editor-skeleton/src/transducers/addon-combine.ts +++ b/packages/editor-skeleton/src/transducers/addon-combine.ts @@ -1,4 +1,5 @@ import { TransformedComponentMetadata, FieldConfig, SettingTarget } from '@ali/lowcode-types'; +import { IconSlot } from '../icons/slot'; export default function(metadata: TransformedComponentMetadata): TransformedComponentMetadata { const { componentName, configure = {} } = metadata; @@ -46,7 +47,7 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp const supportedLifecycles = events.supportedLifecycles || (isRoot - ? [ + ? /*[ { description: '初始化时', name: 'constructor', @@ -63,7 +64,7 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp description: '卸载时', name: 'componentWillUnmount', }, - ] + ]*/ null : null); if (supportedLifecycles) { eventsDefinition.push({ @@ -80,7 +81,22 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp }); } // 通用设置 - const propsGroup = props || []; + let propsGroup = props || []; + const basicInfo: any = {}; + if (componentName === 'Slot') { + basicInfo.icon = IconSlot; + propsGroup = [{ + name: '___title', + title: { + type: 'i18n', + 'en-US': 'Slot Title', + 'zh-CN': '插槽标题' + }, + setter: 'StringSetter', + defaultValue: '插槽容器' + }] + } + /* propsGroup.push({ name: '#generals', title: { type: 'i18n', 'zh-CN': '通用', 'en-US': 'General' }, @@ -101,14 +117,14 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp title: 'Ref', setter: 'StringSetter', }, - /* { name: '!more', title: '更多', setter: 'PropertiesSetter', - },*/ + }, ], }); + */ const combined: FieldConfig[] = [ { title: { type: 'i18n', 'zh-CN': '属性', 'en-US': 'Props' }, @@ -182,8 +198,15 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp items: [ { name: '___condition', - title: { type: 'i18n', 'zh-CN': '条件显示', 'en-US': 'Condition' }, - setter: 'ExpressionSetter', + title: { type: 'i18n', 'zh-CN': '是否渲染', 'en-US': 'Condition' }, + setter: [{ + componentName: 'BoolSetter', + props: { + defaultValue: true, + } + }, { + componentName: 'VariableSetter' + }], }, { name: '#loop', @@ -192,27 +215,14 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp { name: '___loop', title: { type: 'i18n', 'zh-CN': '循环数据', 'en-US': 'Loop Data' }, - setter: { - componentName: 'MixinSetter', + setter: [{ + componentName: 'JsonSetter', 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' }, - }, - }, - ], + label: { type: 'i18n', 'zh-CN': '编辑数据', 'en-US': 'Edit Data'}, }, - }, + }, { + componentName: 'VariableSetter' + }], }, { name: '___loopArgs.0', @@ -236,8 +246,12 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp }, { name: 'key', - title: 'Key', - setter: 'ExpressionSetter', + title: '循环 Key', + setter: [{ + componentName: 'StringSetter', + }, { + componentName: 'VariableSetter' + }], }, ], }, @@ -247,6 +261,7 @@ export default function(metadata: TransformedComponentMetadata): TransformedComp return { ...metadata, + ...basicInfo, configure: { ...configure, combined, diff --git a/packages/editor-skeleton/src/widget/dock.ts b/packages/editor-skeleton/src/widget/dock.ts index c63bbdc2c..4b2081e53 100644 --- a/packages/editor-skeleton/src/widget/dock.ts +++ b/packages/editor-skeleton/src/widget/dock.ts @@ -45,6 +45,8 @@ export default class Dock implements IWidget { } else { this._body = createElement(DockView, props); } + this.inited = true; + return this._body; } diff --git a/packages/editor-skeleton/src/widget/panel-dock.ts b/packages/editor-skeleton/src/widget/panel-dock.ts index e83bbddde..d446eeba6 100644 --- a/packages/editor-skeleton/src/widget/panel-dock.ts +++ b/packages/editor-skeleton/src/widget/panel-dock.ts @@ -59,14 +59,14 @@ export default class PanelDock implements IWidget { this.id = uniqueId(`dock:${name}$`); this.panelName = config.panelName || name; if (content) { + const _panelProps: any = { ...panelProps }; + if (_panelProps.title == null && props) { + _panelProps.title = composeTitle(props.title, undefined, props.description, true, true); + } this._panel = this.skeleton.add({ type: "Panel", name: this.panelName, - props: { - // FIXME! give default title for panel - title: props ? composeTitle(props?.title, props?.icon, props?.description, true) : '', - ...panelProps, - }, + props: _panelProps, contentProps, content, area: panelProps?.area diff --git a/packages/editor-skeleton/src/widget/panel.ts b/packages/editor-skeleton/src/widget/panel.ts index f03e6a130..ff7273683 100644 --- a/packages/editor-skeleton/src/widget/panel.ts +++ b/packages/editor-skeleton/src/widget/panel.ts @@ -1,3 +1,4 @@ +import { EventEmitter } from 'events'; import { createElement, ReactNode } from 'react'; import { obx } from '@ali/lowcode-editor-core'; import { uniqueId, createContent } from '@ali/lowcode-utils'; @@ -13,8 +14,9 @@ export default class Panel implements IWidget { readonly isWidget = true; readonly name: string; readonly id: string; - @obx.ref inited: boolean = false; - @obx.ref private _actived: boolean = false; + @obx.ref inited = false; + @obx.ref private _actived = false; + private emitter = new EventEmitter(); get actived(): boolean { return this._actived; } @@ -46,7 +48,7 @@ export default class Panel implements IWidget { readonly title: TitleContent; readonly help?: HelpTipConfig; - private plain: boolean = false; + private plain = false; private container?: WidgetContainer; private parent?: WidgetContainer; @@ -60,6 +62,9 @@ export default class Panel implements IWidget { this.plain = hideTitleBar || !title; this.help = help; if (Array.isArray(content)) { + if (content.length === 1) { + // todo: not show tabs + } this.container = this.skeleton.createContainer( name, (item) => { @@ -121,7 +126,11 @@ export default class Panel implements IWidget { } active(item?: Panel | string | null) { - this.container?.active(item); + if (item) { + this.container?.active(item); + } else { + this.setActive(true); + } } getName() { @@ -143,9 +152,11 @@ export default class Panel implements IWidget { } this._actived = true; this.parent?.active(this); + this.emitter.emit('activechange', true); } else if (this.inited) { this._actived = false; this.parent?.unactive(this); + this.emitter.emit('activechange', false); } } @@ -179,6 +190,16 @@ export default class Panel implements IWidget { * @deprecated */ setPosition(position: string) { + // noop + } + /** + * @deprecated + */ + onActiveChange(fn: (flag: boolean) => void): () => void { + this.emitter.on('activechange', fn); + return () => { + this.emitter.removeListener('activechange', fn); + }; } } diff --git a/packages/editor-skeleton/src/widget/utils.ts b/packages/editor-skeleton/src/widget/utils.ts index b635fbbe0..b90c33df8 100644 --- a/packages/editor-skeleton/src/widget/utils.ts +++ b/packages/editor-skeleton/src/widget/utils.ts @@ -1,7 +1,7 @@ -import { IconType, TitleContent, isI18nData, TipContent } from '@ali/lowcode-types'; +import { IconType, TitleContent, isI18nData, TipContent, isTitleConfig } from '@ali/lowcode-types'; import { isValidElement } from 'react'; -export function composeTitle(title?: TitleContent, icon?: IconType, tip?: TipContent, tipAsTitle?: boolean) { +export function composeTitle(title?: TitleContent, icon?: IconType, tip?: TipContent, tipAsTitle?: boolean, noIcon?: boolean) { if (!title) { title = {}; if (!icon || tipAsTitle) { @@ -11,6 +11,19 @@ export function composeTitle(title?: TitleContent, icon?: IconType, tip?: TipCon } if (icon || tip) { if (typeof title !== 'object' || isValidElement(title) || isI18nData(title)) { + if (isValidElement(title)) { + if (title.type === 'svg' || (title.type as any).getIcon) { + if (!icon) { + icon = title as any; + } + if (tipAsTitle) { + title = tip as any; + tip = null; + } else { + title = undefined; + } + } + } title = { label: title, icon, @@ -24,5 +37,8 @@ export function composeTitle(title?: TitleContent, icon?: IconType, tip?: TipCon }; } } + if (isTitleConfig(title) && noIcon) { + title.icon = undefined; + } return title; } diff --git a/packages/editor-skeleton/src/widget/widget-container.ts b/packages/editor-skeleton/src/widget/widget-container.ts index 268f5fcfb..522648301 100644 --- a/packages/editor-skeleton/src/widget/widget-container.ts +++ b/packages/editor-skeleton/src/widget/widget-container.ts @@ -39,7 +39,7 @@ export default class WidgetContainer
- + <Title title={{ type: 'i18n', intl: intlNode('Slots') }} /> </div> {treeNode.slots.map(tnode => ( <TreeNodeView key={tnode.id} treeNode={tnode} /> diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx index 3d7c6b373..2417102ae 100644 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -5,12 +5,11 @@ import { IconArrowRight } from '../icons/arrow-right'; import { IconEyeClose } from '../icons/eye-close'; import { IconLock } from '../icons/lock'; import { IconUnlock } from '../icons/unlock'; -import { intl } from '../locale'; +import { intl, intlNode } from '../locale'; import TreeNode from '../tree-node'; import { IconEye } from '../icons/eye'; import { IconCond } from '../icons/cond'; import { IconLoop } from '../icons/loop'; -import { IconSlot } from '../icons/slot'; import { createIcon } from '@ali/lowcode-utils'; @observer @@ -104,29 +103,28 @@ export default class TreeTitle extends Component<{ {node.slotFor && ( <a className="tree-node-tag slot"> {/* todo: click redirect to prop */} - <IconSlot /> - <Tip>{intl('Slot for {prop}', { prop: node.slotFor.key })}</Tip> + <Tip>{intlNode('Slot for {prop}', { prop: node.slotFor.key })}</Tip> </a> )} {node.hasLoop() && ( <a className="tree-node-tag loop"> {/* todo: click todo something */} <IconLoop /> - <Tip>{intl('Loop')}</Tip> + <Tip>{intlNode('Loop')}</Tip> </a> )} {node.hasCondition() && !node.conditionGroup && ( <a className="tree-node-tag cond"> {/* todo: click todo something */} <IconCond /> - <Tip>{intl('Conditional')}</Tip> + <Tip>{intlNode('Conditional')}</Tip> </a> )} </Fragment> )} </div> {isCNode && isNodeParent && <HideBtn treeNode={treeNode} />} - {isCNode && isNodeParent && <LockBtn treeNode={treeNode} />} + {/*isCNode && isNodeParent && <LockBtn treeNode={treeNode} />*/} </div> ); } diff --git a/packages/plugin-outline-pane/tsconfig.json b/packages/plugin-outline-pane/tsconfig.json index c37b76ecc..af656bc8f 100644 --- a/packages/plugin-outline-pane/tsconfig.json +++ b/packages/plugin-outline-pane/tsconfig.json @@ -4,6 +4,6 @@ "outDir": "lib" }, "include": [ - "./src/" + "./src/", ] } diff --git a/packages/react-renderer/src/engine/base.jsx b/packages/react-renderer/src/engine/base.jsx index 6bf45fe3f..14059f015 100644 --- a/packages/react-renderer/src/engine/base.jsx +++ b/packages/react-renderer/src/engine/base.jsx @@ -228,6 +228,10 @@ export default class BaseEngine extends PureComponent { let Comp = components[schema.componentName] || Div; + if (schema.hidden) { + return null; + } + if (schema.loop !== undefined) { return this.__createLoopVirtualDom( { diff --git a/packages/react-simulator-renderer/package.json b/packages/react-simulator-renderer/package.json index d031eb62c..b62d92110 100644 --- a/packages/react-simulator-renderer/package.json +++ b/packages/react-simulator-renderer/package.json @@ -21,6 +21,7 @@ "@recore/obx-react": "^1.0.7", "classnames": "^2.2.6", "react": "^16", + "@ali/vu-css-style": "^1.0.2", "react-dom": "^16.7.0" }, "devDependencies": { diff --git a/packages/react-simulator-renderer/src/builtin-components/slot.tsx b/packages/react-simulator-renderer/src/builtin-components/slot.tsx index 107864d52..cf67dbcf2 100644 --- a/packages/react-simulator-renderer/src/builtin-components/slot.tsx +++ b/packages/react-simulator-renderer/src/builtin-components/slot.tsx @@ -6,7 +6,7 @@ class Slot extends Component { componentName: 'Slot', configure: { props: [{ - name: '___title___', + name: '___title', title: { type: 'i18n', 'en-US': 'Slot Title', @@ -15,7 +15,7 @@ class Slot extends Component { setter: 'StringSetter', defaultValue: '插槽容器' }, { - name: '___params___', + name: '___params', title: { type: 'i18n', 'en-US': 'Slot Params', diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index 7b41312c8..5ae9ce0fe 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -1,4 +1,5 @@ import LowCodeRenderer from '@ali/lowcode-react-renderer'; +import { isObject } from 'lodash'; import { ReactInstance, Fragment, Component, createElement } from 'react'; import { observer } from '@recore/obx-react'; import { SimulatorRenderer } from './renderer'; @@ -7,7 +8,7 @@ import './renderer.less'; // patch cloneElement avoid lost keyProps const originCloneElement = window.React.cloneElement; -(window as any).React.cloneElement = (child: any, { _leaf, ...props}: any = {}) => { +(window as any).React.cloneElement = (child: any, { _leaf, ...props }: any = {}) => { if (child.ref && props.ref) { const dRef = props.ref; const cRef = child.ref; @@ -18,7 +19,7 @@ const originCloneElement = window.React.cloneElement; } else { try { cRef.current = x; - } catch (e) { } + } catch (e) {} } } if (dRef) { @@ -27,13 +28,13 @@ const originCloneElement = window.React.cloneElement; } else { try { dRef.current = x; - } catch (e) { } + } catch (e) {} } } - } - }; + }; + } return originCloneElement(child, props); -} +}; export default class SimulatorRendererView extends Component<{ renderer: SimulatorRenderer }> { render() { @@ -84,10 +85,11 @@ class Renderer extends Component<{ renderer: SimulatorRenderer }> { const { __id, __desingMode, ...viewProps } = props; viewProps.componentId = __id; viewProps._leaf = host.document.getNode(__id); + return createElement( Component, viewProps, - children == null ? null : Array.isArray(children) ? children : [children], + children == null ? [] : Array.isArray(children) ? children : [children], ); }} onCompGetRef={(schema: any, ref: ReactInstance | null) => { diff --git a/packages/types/src/title.ts b/packages/types/src/title.ts index 3be21d84e..7a260ebeb 100644 --- a/packages/types/src/title.ts +++ b/packages/types/src/title.ts @@ -1,5 +1,5 @@ import { ReactElement, ReactNode } from 'react'; -import { I18nData } from './i18n'; +import { I18nData, isI18nData } from './i18n'; import { TipContent } from './tip'; import { IconType } from './icon'; @@ -13,3 +13,14 @@ export interface TitleConfig { export type TitleContent = string | I18nData | ReactElement | TitleConfig; +function isPlainObject(value: any): value is object { + if (typeof value !== 'object') { + return false; + } + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null; +} + +export function isTitleConfig(obj: any): obj is TitleConfig { + return isPlainObject(obj) && !isI18nData(obj); +} diff --git a/packages/utils/src/is-plain-object.ts b/packages/utils/src/is-plain-object.ts index 899ec5c64..59260d6cb 100644 --- a/packages/utils/src/is-plain-object.ts +++ b/packages/utils/src/is-plain-object.ts @@ -1,6 +1,6 @@ import { isObject } from './is-object'; -export function isPlainObject(value: any) { +export function isPlainObject(value: any): value is object { if (!isObject(value)) { return false; } diff --git a/packages/vision-preset/src/bundle/bundle.ts b/packages/vision-preset/src/bundle/bundle.ts index 6e00f7e58..d74c3de12 100644 --- a/packages/vision-preset/src/bundle/bundle.ts +++ b/packages/vision-preset/src/bundle/bundle.ts @@ -100,6 +100,22 @@ export default class Bundle { cp.setView(view); } + /** + * TODO dirty fix + */ + addComponentBundle(bundles: any) { + /** + * Normal Component bundle: [ Prototype, PrototypeView ] + * Component without Prototype.js: [ View ] + */ + if (bundles.length >= 2) { + const prototype = bundles[0]; + const prototypeView = bundles[1]; + prototype.setView(prototypeView); + this.registerPrototype(prototype); + } + } + private recursivelyRegisterViews(list: any[], viewName?: string): void { list.forEach((item: any) => { if (Array.isArray(item.module)) { diff --git a/packages/vision-preset/src/bundle/upgrade-metadata.ts b/packages/vision-preset/src/bundle/upgrade-metadata.ts index 3ea38d90e..b439559a4 100644 --- a/packages/vision-preset/src/bundle/upgrade-metadata.ts +++ b/packages/vision-preset/src/bundle/upgrade-metadata.ts @@ -237,7 +237,7 @@ export function upgradePropConfig(config: OldPropConfig, addInitial: AddIntial) } } - if (collapse || collapsed || fieldCollapsed) { + if (collapse || collapsed || fieldCollapsed || extraProps.display === DISPLAY_TYPE.ENTRY) { extraProps.defaultCollapsed = true; } function isDisabled(field: Field) { @@ -287,10 +287,14 @@ export function upgradePropConfig(config: OldPropConfig, addInitial: AddIntial) if (slotName && initialValue === true) { initialFn = (field: any, value: any) => { if (isJSSlot(value)) { - return value; + return { + title: slotTitle || title, + ...value, + }; } return { type: 'JSSlot', + title: slotTitle || title, value: initialChildren, }; }; @@ -351,10 +355,14 @@ export function upgradePropConfig(config: OldPropConfig, addInitial: AddIntial) componentName: 'SlotSetter', initialValue: (field: any, value: any) => { if (isJSSlot(value)) { - return value; + return { + title: slotTitle || title, + ...value, + }; } return { type: 'JSSlot', + title: slotTitle || title, value: value == null ? initialChildren : value, }; }, diff --git a/packages/vision-preset/src/components/index.less b/packages/vision-preset/src/components/index.less new file mode 100644 index 000000000..c630e0914 --- /dev/null +++ b/packages/vision-preset/src/components/index.less @@ -0,0 +1,82 @@ +@import '~@ali/ve-less-variables/index.less'; + +// 样式直接沿用之前的样式,优化了下命名 +.instance-node-selector { + position: relative; + margin-right: 2px; + color: var(--color-icon-white, @title-bgcolor); + border-radius: @global-border-radius; + margin-right: 2px; + pointer-events: auto; + flex-grow: 0; + flex-shrink: 0; + + svg { + width: 16px; + height: 16px; + margin-right: 5px; + flex-grow: 0; + flex-shrink: 0; + max-width: inherit; + path { + fill: var(--color-icon-white, @title-bgcolor); + } + } + &-current { + background: var(--color-brand, @brand-color-1); + padding: 0 6px; + display: flex; + align-items: center; + height: 20px; + cursor: pointer; + color: var(--color-icon-white, @title-bgcolor); + border-radius: 3px; + + &-title { + padding-right: 6px; + color: var(--color-icon-white, @title-bgcolor); + } + } + &-list { + position: absolute; + left: 0; + right: 0; + opacity: 0; + visibility: hidden; + } + &-node { + margin: 2px 0; + &-content { + padding-left: 6px; + background: #78869a; + display: inline-flex; + border-radius: 3px; + align-items: center; + height: 20px; + color: var(--color-icon-white, @title-bgcolor); + cursor: pointer; + overflow: visible; + } + &-title { + padding-right: 6px; + // margin-left: 5px; + color: var(--color-icon-white, @title-bgcolor); + cursor: pointer; + overflow: visible; + } + &:hover { + opacity: 0.8; + } + } +} + +&:hover { + .instance-node-selector-current { + color: ar(--color-text-reverse, @white-alpha-2); + } + .instance-node-selector-popup { + visibility: visible; + opacity: 1; + transition: 0.2s all ease-in; + } +} diff --git a/packages/vision-preset/src/components/index.tsx b/packages/vision-preset/src/components/index.tsx new file mode 100644 index 000000000..f32681dd0 --- /dev/null +++ b/packages/vision-preset/src/components/index.tsx @@ -0,0 +1,110 @@ +import { Overlay } from '@alifd/next'; +import React from 'react'; +import './index.less'; +import { Title } from '@ali/lowcode-editor-core'; + +import { Node, ParentalNode } from '@ali/lowcode-designer'; + +const { Popup } = Overlay; + +export interface IProps { + node: Node; +} + +export interface IState { + parentNodes: Node[]; +} + +type UnionNode = Node | ParentalNode | null; + +export class InstanceNodeSelector extends React.Component<IProps, IState> { + state: IState = { + parentNodes: [], + }; + + componentDidMount() { + const parentNodes = this.getParentNodes(this.props.node); + this.setState({ + parentNodes, + }); + } + + // 获取节点的父级节点(最多获取5层) + getParentNodes = (node: Node) => { + const parentNodes = []; + let currentNode: UnionNode = node; + + while (currentNode && parentNodes.length < 5) { + currentNode = currentNode.getParent(); + if (currentNode) { + parentNodes.push(currentNode); + } + } + return parentNodes; + }; + + onSelect = (node: Node) => () => { + if (node && typeof node.select === 'function') { + node.select(); + } + }; + onMouseOver = (node: Node) => (_: any, flag = true) => { + if (node && typeof node.hover === 'function') { + node.hover(flag); + } + }; + onMouseOut = (node: Node) => (_: any, flag = false) => { + if (node && typeof node.hover === 'function') { + node.hover(flag); + } + }; + renderNodes = (node: Node) => { + const nodes = this.state.parentNodes || []; + const children = nodes.map((node, key) => { + return ( + <div + key={key} + onClick={this.onSelect(node)} + onMouseEnter={this.onMouseOver(node)} + onMouseLeave={this.onMouseOut(node)} + className="instance-node-selector-node" + > + <div className="instance-node-selector-node-content"> + <Title + className="instance-node-selector-node-title" + title={{ + label: node.title, + icon: node.icon, + }} + /> + </div> + </div> + ); + }); + return children; + }; + + render() { + const { node } = this.props; + return ( + <div className="instance-node-selector"> + <Popup + trigger={ + <div className="instance-node-selector-current"> + <Title + className="instance-node-selector-node-title" + title={{ + label: node.title, + icon: node.icon, + }} + /> + </div> + } + triggerType="hover" + > + <div className="instance-node-selector">{this.renderNodes(node)}</div> + </Popup> + </div> + ); + } +} diff --git a/packages/vision-preset/src/editor.ts b/packages/vision-preset/src/editor.ts index 48c6e4def..5dac4dfd4 100644 --- a/packages/vision-preset/src/editor.ts +++ b/packages/vision-preset/src/editor.ts @@ -1,15 +1,16 @@ import { isJSBlock, isJSSlot } from '@ali/lowcode-types'; import { isPlainObject } from '@ali/lowcode-utils'; -import { globalContext, Editor, registerSetter } from '@ali/lowcode-editor-core'; -import { Designer, TransformStage } from '@ali/lowcode-designer'; +import { globalContext, Editor } from '@ali/lowcode-editor-core'; +import { Designer, TransformStage, addBuiltinComponentAction } from '@ali/lowcode-designer'; // import { registerSetters } from '@ali/lowcode-setters'; import Outline from '@ali/lowcode-plugin-outline-pane'; +import { toCss } from '@ali/vu-css-style'; + import DesignerPlugin from '@ali/lowcode-plugin-designer'; import { Skeleton, SettingsPrimaryPane } from '@ali/lowcode-editor-skeleton'; -import Preview from '@ali/lowcode-plugin-sample-preview'; -// import SourceEditor from '@ali/lowcode-plugin-source-editor'; import { i18nReducer } from './i18n-reducer'; +import { InstanceNodeSelector } from './components'; export const editor = new Editor(); globalContext.register(editor, Editor); @@ -66,6 +67,37 @@ function upgradePropsReducer(props: any) { } designer.addPropsReducer(upgradePropsReducer, TransformStage.Init); +// 设计器组件样式处理 +function stylePropsReducer(props: any, node: any) { + if (props && typeof props === 'object' && props.__style__) { + const doc = designer.currentDocument?.simulator?.contentDocument; + if (!doc) { + return; + } + const cssId = '_style_pesudo_' + node.id.replace(/\$/g, '_'); + const cssClass = '_css_pesudo_' + node.id.replace(/\$/g, '_'); + const dom = doc.getElementById(cssId); + if (dom) { + dom.parentNode?.removeChild(dom); + } + let styleProp = props.__style__; + if (typeof styleProp === 'object') { + styleProp = toCss(styleProp); + } + if (typeof styleProp === 'string') { + const s = doc.createElement('style'); + props.className = cssClass; + s.setAttribute('type', 'text/css'); + s.setAttribute('id', cssId); + doc.getElementsByTagName('head')[0].appendChild(s); + + s.appendChild(doc.createTextNode(styleProp.replace(/:root/g, '.' + cssClass))); + } + } + return props; +} +designer.addPropsReducer(stylePropsReducer, TransformStage.Render); + skeleton.add({ area: 'mainArea', name: 'designer', @@ -88,16 +120,6 @@ skeleton.add({ }, }); -skeleton.add({ - area: 'topArea', - type: 'Dock', - name: 'preview', - props: { - align: 'right', - }, - content: Preview, -}); - // skeleton.add({ // name: 'sourceEditor', // type: 'PanelDock', @@ -112,3 +134,11 @@ skeleton.add({ // }, // content: SourceEditor, // }); + +// 实例节点选择器,线框高亮 +addBuiltinComponentAction({ + name: 'instance-node-selector', + content: InstanceNodeSelector, + important: true, + condition: 'always' +}); diff --git a/packages/vision-preset/src/env.ts b/packages/vision-preset/src/env.ts index bd5ab894a..8c7eae39b 100644 --- a/packages/vision-preset/src/env.ts +++ b/packages/vision-preset/src/env.ts @@ -1,13 +1,13 @@ import { EventEmitter } from 'events'; import { ALI_SCHEMA_VERSION } from './base/const'; +import { obx } from '@ali/lowcode-editor-core'; interface ILiteralObject { [key: string]: any; } export class Env { - - public envs: ILiteralObject; + @obx.val envs: ILiteralObject = {}; private emitter: EventEmitter; private featureMap: ILiteralObject; @@ -15,23 +15,22 @@ export class Env { constructor() { this.emitter = new EventEmitter(); this.emitter.setMaxListeners(0); - this.envs = {}; this.featureMap = {}; } - public get(name: string): any { + get(name: string): any { return this.getEnv(name); } - public getEnv(name: string): any { + getEnv(name: string): any { return this.envs[name]; } - public set(name: string, value: any) { + set(name: string, value: any) { return this.setEnv(name, value); } - public setEnv(name: string, value: any) { + setEnv(name: string, value: any) { const orig = this.envs[name]; if (JSON.stringify(orig) === JSON.stringify(value)) { return; @@ -40,47 +39,47 @@ export class Env { this.emitter.emit('envchange', this.envs, name, value); } - public setEnvMap(envs: ILiteralObject): void { + setEnvMap(envs: ILiteralObject): void { this.envs = Object.assign(this.envs, envs); this.emitter.emit('envchange', this.envs); } - public getLocale(): string { + getLocale(): string { return this.getEnv('locale') || 'zh_CN'; } - public setLocale(locale: string) { + setLocale(locale: string) { this.setEnv('locale', locale); } - public setExpertMode(flag: string) { + setExpertMode(flag: string) { this.setEnv('expertMode', !!flag); } - public isExpertMode() { + isExpertMode() { return !!this.getEnv('expertMode'); } - public getSupportFeatures() { + getSupportFeatures() { return Object.assign({}, this.featureMap); } - public setSupportFeatures(features: ILiteralObject) { + setSupportFeatures(features: ILiteralObject) { this.featureMap = Object.assign({}, this.featureMap, features); } - public supports(name = 'supports') { + supports(name = 'supports') { return !!this.featureMap[name]; } - public onEnvChange(func: (envs: ILiteralObject, name: string, value: any) => any) { + onEnvChange(func: (envs: ILiteralObject, name: string, value: any) => any) { this.emitter.on('envchange', func); return () => { this.emitter.removeListener('envchange', func); }; } - public getAliSchemaVersion() { + getAliSchemaVersion() { return ALI_SCHEMA_VERSION; } } diff --git a/packages/vision-preset/src/exchange.ts b/packages/vision-preset/src/exchange.ts index df71cff14..6e1f8c13e 100644 --- a/packages/vision-preset/src/exchange.ts +++ b/packages/vision-preset/src/exchange.ts @@ -12,4 +12,13 @@ export default { const nodes = designer.currentSelection?.getNodes(); return nodes?.[0]; }, + /** + * TODO dirty fix + */ + onIntoView(func: (node: any, insertion: any) => any) { + // this.emitter.on('intoview', func); + return () => { + // this.emitter.removeListener('intoview', func); + }; + } } diff --git a/packages/vision-preset/src/i18n-reducer.ts b/packages/vision-preset/src/i18n-reducer.ts index 693ee0cae..a1a153932 100644 --- a/packages/vision-preset/src/i18n-reducer.ts +++ b/packages/vision-preset/src/i18n-reducer.ts @@ -1,5 +1,5 @@ -const I18nUtil = require('@ali/ve-i18n-util'); import Env from './env'; +const I18nUtil = require('@ali/ve-i18n-util'); interface I18nObject { type?: string; @@ -9,7 +9,9 @@ interface I18nObject { } export function i18nReducer(obj?: any): any { - if (!obj) { return obj; } + if (!obj) { + return obj; + } if (Array.isArray(obj)) { return obj.map((item) => i18nReducer(item)); } @@ -18,6 +20,7 @@ export function i18nReducer(obj?: any): any { // FIXME! use editor.get let locale = Env.getLocale(); if (obj.key) { + // FIXME: 此处需要升级I18nUtil,改成响应式 return I18nUtil.get(obj.key, locale); } if (locale !== 'zh_CN' && locale !== 'zh_TW' && !obj[locale]) { diff --git a/packages/vision-preset/src/index.ts b/packages/vision-preset/src/index.ts index 2c4db40e9..ed632b3ef 100644 --- a/packages/vision-preset/src/index.ts +++ b/packages/vision-preset/src/index.ts @@ -1,11 +1,12 @@ import * as utils from '@ali/ve-utils'; import Popup from '@ali/ve-popups'; import Icons from '@ali/ve-icons'; +import logger from '@ali/vu-logger'; import { render } from 'react-dom'; import I18nUtil from '@ali/ve-i18n-util'; import { hotkey as Hotkey } from '@ali/lowcode-editor-core'; import { createElement } from 'react'; -import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS } from './base/const'; +import { VE_EVENTS as EVENTS, VE_HOOKS as HOOKS, VERSION as Version } from './base/const'; import Bus from './bus'; import { skeleton } from './editor'; import { Workbench } from '@ali/lowcode-editor-skeleton'; @@ -21,7 +22,10 @@ import * as Field from './fields'; import Prop from './prop'; import Env from './env'; import DragEngine from './drag-engine'; +import Viewport from './viewport'; +import Project from './project'; import { designer, editor } from './editor'; +import Symbols from './symbols'; import './vision.less'; @@ -41,6 +45,7 @@ function init(container?: Element) { createElement(Workbench, { skeleton, className: 'engine-main', + topAreaItemClassName: 'engine-actionitem', }), container, ); @@ -101,6 +106,11 @@ const VisualEngine = { Bundle, Pages, DragEngine, + Viewport, + Version, + Project, + logger, + Symbols, }; (window as any).VisualEngine = VisualEngine; @@ -144,6 +154,11 @@ export { Bundle, Pages, DragEngine, + Viewport, + Version, + Project, + logger, + Symbols, }; diff --git a/packages/vision-preset/src/pages.ts b/packages/vision-preset/src/pages.ts index 81430a4f1..c269f2471 100644 --- a/packages/vision-preset/src/pages.ts +++ b/packages/vision-preset/src/pages.ts @@ -6,17 +6,28 @@ const { project } = designer; export interface OldPageData { id: string; - layout: RootSchema; + componentsTree: RootSchema[]; [dataAddon: string]: any; } const pages = Object.assign(project, { setPages(pages: OldPageData[]) { + if (!pages || !Array.isArray(pages) || pages.length === 0) { + throw new Error('pages schema 不合法'); + } + + if (pages[0].componentsTree[0]) { + pages[0].componentsTree[0].componentName = 'Page'; + // FIXME + pages[0].componentsTree[0].lifeCycles = {}; + pages[0].componentsTree[0].methods = {}; + } + project.load({ version: '1.0.0', componentsMap: [], - componentsTree: pages.map(page => page.layout), - }); + componentsTree: pages[0].componentsTree, + }, true); }, addPage(data: OldPageData) { return project.open(data.layout); diff --git a/packages/vision-preset/src/panes.ts b/packages/vision-preset/src/panes.ts index 3604e6186..3be27f531 100644 --- a/packages/vision-preset/src/panes.ts +++ b/packages/vision-preset/src/panes.ts @@ -69,17 +69,7 @@ function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: strin newConfig.type = 'PanelDock'; newConfig.area = 'left'; newConfig.props.description = description || title; - const { - contents, - hideTitleBar, - tip, - width, - maxWidth, - height, - maxHeight, - menu, - isAction - } = config; + const { contents, hideTitleBar, tip, width, maxWidth, height, maxHeight, menu, isAction } = config; if (menu) { newConfig.props.title = menu; } @@ -95,15 +85,16 @@ function upgradeConfig(config: OldPaneConfig): IWidgetBaseConfig & { area: strin }; if (contents && Array.isArray(contents)) { - newConfig.content = contents.map(({ title, content, tip }) => { + newConfig.content = contents.map(({ title, content, tip }, index) => { return { - type: "Panel", + type: 'Panel', + name: typeof title === 'string' ? title : `${name}:${index}`, content, props: { title, help: tip, - } - } + }, + }; }); } } @@ -162,7 +153,11 @@ const dockPane = Object.assign(skeleton.leftArea, { return; } const name = item.name || item; - skeleton.getPanel(name)?.active(); + const pane = skeleton.getPanel(name); + if (!pane) { + console.warn(`Could not find pane with name ${name}`); + } + pane?.active(); }, /** diff --git a/packages/vision-preset/src/project.ts b/packages/vision-preset/src/project.ts new file mode 100644 index 000000000..ba5d2309b --- /dev/null +++ b/packages/vision-preset/src/project.ts @@ -0,0 +1,17 @@ +class Project { + private schema: any; + + constructor() { + this.schema = {}; + } + + getSchema() { + return this.schema; + } + + setSchema(schema: any) { + this.schema = schema; + } +} + +export default new Project(); diff --git a/packages/vision-preset/src/prop.ts b/packages/vision-preset/src/prop.ts index 478fe0ec2..8d9abdfbf 100644 --- a/packages/vision-preset/src/prop.ts +++ b/packages/vision-preset/src/prop.ts @@ -2,11 +2,12 @@ import { Component } from 'react'; import { EventEmitter } from 'events'; import { fromJS, Iterable, Map as IMMap } from 'immutable'; import logger from '@ali/vu-logger'; -import { uniqueId, cloneDeep, isDataEqual, combineInitial, Transducer } from '@ali/ve-utils'; +import { cloneDeep, isDataEqual, combineInitial, Transducer } from '@ali/ve-utils'; import I18nUtil from '@ali/ve-i18n-util'; import { getSetter } from '@ali/lowcode-editor-core'; import { editor } from './editor'; import { OldPropConfig, DISPLAY_TYPE } from './bundle/upgrade-metadata'; +import { uniqueId } from '@ali/lowcode-utils'; type IPropConfig = OldPropConfig; @@ -108,7 +109,7 @@ export default class Prop implements IVariableSettable { this.parent = parent; } - this.id = uniqueId(null as any, 'prop', 'engine-prop'); + this.id = uniqueId('prop'); if (typeof config.setter === 'string') { config.setter = getSetter(config.setter)?.component as any; diff --git a/packages/vision-preset/src/symbols.ts b/packages/vision-preset/src/symbols.ts new file mode 100644 index 000000000..81091e371 --- /dev/null +++ b/packages/vision-preset/src/symbols.ts @@ -0,0 +1,17 @@ +export class SymbolManager { + private symbolMap: { [symbolName: string]: symbol } = {}; + + public create(name: string): symbol { + if (this.symbolMap[name]) { + return this.symbolMap[name]; + } + this.symbolMap[name] = Symbol(name); + return this.symbolMap[name]; + } + + public get(name: string) { + return this.symbolMap[name]; + } +} + +export default new SymbolManager(); diff --git a/packages/vision-preset/src/viewport.ts b/packages/vision-preset/src/viewport.ts new file mode 100644 index 000000000..0b5ec7cb7 --- /dev/null +++ b/packages/vision-preset/src/viewport.ts @@ -0,0 +1,278 @@ +import { EventEmitter } from 'events'; + +const domReady = require('domready'); +import Flags from './flags'; + +function enterFullscreen() { + const elem = document.documentElement; + if (elem.requestFullscreen) { + elem.requestFullscreen(); + } +} + +function exitFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } +} + +function isFullscreen() { + return document.fullscreen || false; +} + +interface IStyleResourceConfig { + media?: string; + type?: string; + content?: string; +} + +class StyleResource { + config: IStyleResourceConfig; + styleElement: HTMLStyleElement; + mounted: boolean; + inited: boolean; + + constructor(config: IStyleResourceConfig) { + this.config = config || {}; + } + + matchDevice(device: string) { + const media = this.config.media; + + if (!media || media === 'ALL' || media === '*') { + return true; + } + + return media.toUpperCase() === device.toUpperCase(); + } + + init() { + if (this.inited) { + return; + } + + this.inited = true; + + const { type, content } = this.config; + + let styleElement; + if (type === 'URL') { + styleElement = document.createElement('link'); + styleElement.href = content || ''; + styleElement.rel = 'stylesheet'; + } else { + styleElement = document.createElement('style'); + styleElement.setAttribute('type', 'text/css'); + if (styleElement.styleSheet) { + styleElement.styleSheet.cssText = content; + } else { + styleElement.appendChild(document.createTextNode(content || '')); + } + } + this.styleElement = styleElement; + } + + apply() { + if (this.mounted) { + return; + } + + this.init(); + document.head.appendChild(this.styleElement); + this.mounted = true; + } + + unmount() { + if (!this.mounted) { + return; + } + document.head.removeChild(this.styleElement); + this.mounted = false; + } +} + +export class Viewport { + preview: boolean; + focused: boolean; + slateFixed: boolean; + emitter: EventEmitter; + device: string; + focusTarget: any; + cssResourceSet: StyleResource[]; + + constructor() { + this.preview = false; + this.emitter = new EventEmitter(); + document.addEventListener('webkitfullscreenchange', () => { + this.emitter.emit('fullscreenchange', this.isFullscreen()); + }); + domReady(() => this.applyMediaCSS()); + } + + setFullscreen(flag: boolean) { + const fullscreen = this.isFullscreen(); + if (fullscreen && !flag) { + exitFullscreen(); + } else if (!fullscreen && flag) { + enterFullscreen(); + } + } + + toggleFullscreen() { + if (this.isFullscreen()) { + exitFullscreen(); + } else { + enterFullscreen(); + } + } + + isFullscreen() { + return isFullscreen(); + } + + setFocus(flag: boolean) { + if (this.focused && !flag) { + this.focused = false; + Flags.remove('view-focused'); + this.emitter.emit('focuschange', false); + } else if (!this.focused && flag) { + this.focused = true; + Flags.add('view-focused'); + this.emitter.emit('focuschange', true); + } + } + + setFocusTarget(focusTarget: any) { + this.focusTarget = focusTarget; + } + + returnFocus() { + if (this.focusTarget) { + this.focusTarget.focus(); + } + } + + isFocus() { + return this.focused; + } + + setPreview(flag: boolean) { + if (this.preview && !flag) { + this.preview = false; + Flags.setPreviewMode(false); + this.emitter.emit('preview', false); + this.changeViewport(); + } else if (!this.preview && flag) { + this.preview = true; + Flags.setPreviewMode(true); + this.emitter.emit('preview', true); + this.changeViewport(); + } + } + + togglePreview() { + if (this.isPreview()) { + this.setPreview(false); + } else { + this.setPreview(true); + } + } + + isPreview() { + return this.preview; + } + + setDevice(device = 'pc') { + if (this.getDevice() !== device) { + this.device = device; + Flags.setSimulator(device); + this.applyMediaCSS(); + this.emitter.emit('devicechange', device); + this.changeViewport(); + } + } + + getDevice() { + return this.device || 'pc'; + } + + changeViewport() { + this.emitter.emit('viewportchange', this.getViewport()); + } + + getViewport() { + return `${this.isPreview() ? 'preview' : 'design'}-${this.getDevice()}`; + } + + applyMediaCSS() { + if (!document.head || !this.cssResourceSet) { + return; + } + const device = this.getDevice(); + this.cssResourceSet.forEach((item) => { + if (item.matchDevice(device)) { + item.apply(); + } else { + item.unmount(); + } + }); + } + + setGlobalCSS(resourceSet: IStyleResourceConfig[]) { + if (this.cssResourceSet) { + this.cssResourceSet.forEach((item) => { + item.unmount(); + }); + } + this.cssResourceSet = resourceSet.map((item: IStyleResourceConfig) => new StyleResource(item)).reverse(); + this.applyMediaCSS(); + } + + setWithShell(shell: string) { + Flags.setWithShell(shell); + } + + onFullscreenChange(func: () => any) { + this.emitter.on('fullscreenchange', func); + return () => { + this.emitter.removeListener('fullscreenchange', func); + }; + } + + onPreview(func: () => any) { + this.emitter.on('preview', func); + return () => { + this.emitter.removeListener('preview', func); + }; + } + + onDeviceChange(func: () => any) { + this.emitter.on('devicechange', func); + return () => { + this.emitter.removeListener('devicechange', func); + }; + } + + onSlateFixedChange(func: (flag: boolean) => any) { + this.emitter.on('slatefixed', func); + return () => { + this.emitter.removeListener('slatefixed', func); + }; + } + + onViewportChange(func: () => any) { + this.emitter.on('viewportchange', func); + return () => { + this.emitter.removeListener('viewportchange', func); + }; + } + + onFocusChange(func: (flag: boolean) => any) { + this.emitter.on('focuschange', func); + return () => { + this.emitter.removeListener('focuschange', func); + }; + } +} + +export default new Viewport(); diff --git a/packages/vision-preset/src/vision.less b/packages/vision-preset/src/vision.less index d0c17fcc0..d188878a5 100644 --- a/packages/vision-preset/src/vision.less +++ b/packages/vision-preset/src/vision.less @@ -92,7 +92,8 @@ html.engine-blur #engine { .next-checkbox-group,.next-date-picker,.next-input,.next-month-picker, .next-number-picker,.next-radio-group,.next-range,.next-range-picker, .next-rating,.next-select,.next-switch,.next-time-picker,.next-upload, - .next-year-picker { + .next-year-picker, + .next-breadcrumb-item,.next-calendar-header,.next-calendar-table { pointer-events: auto !important; } } @@ -100,3 +101,9 @@ html.engine-blur #engine { .lc-left-float-pane { font-size: 14px; } + +html.engine-preview-mode { + .lc-left-area, .lc-right-area { + display: none !important; + } +}