diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index 1954f6091..0de4a9224 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,7 @@ import { IconPage } from './icons/page'; import { IconComponent } from './icons/component'; import { IconRemove } from './icons/remove'; import { IconClone } from './icons/clone'; +import { ReactElement } from 'react'; function ensureAList(list?: string | string[]): string[] | null { if (!list) { @@ -91,12 +94,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 +142,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; } 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/document/node/node.ts b/packages/designer/src/document/node/node.ts index 4cc95b4e1..a8d469309 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()}`; diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index 227cdd6dc..9e76eee34 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -7,7 +7,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 +18,7 @@ const originCloneElement = window.React.cloneElement; } else { try { cRef.current = x; - } catch (e) { } + } catch (e) {} } } if (dRef) { @@ -27,13 +27,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() { @@ -97,7 +97,7 @@ class Renderer extends Component<{ renderer: SimulatorRenderer }> { 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..658ddf73a 100644 --- a/packages/types/src/title.ts +++ b/packages/types/src/title.ts @@ -13,3 +13,6 @@ export interface TitleConfig { export type TitleContent = string | I18nData | ReactElement | TitleConfig; +export function isTitleConfig(obj: any): obj is TitleConfig { + return obj && (obj.label || obj.tip || obj.icon); +} 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/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..2518705aa --- /dev/null +++ b/packages/vision-preset/src/components/index.tsx @@ -0,0 +1,95 @@ +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 { + 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(); + } + }; + + renderNodes = (node: Node) => { + const nodes = this.state.parentNodes || []; + const children = nodes.map((node, key) => { + return ( +
+
+ + </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..5c064a3b0 100644 --- a/packages/vision-preset/src/editor.ts +++ b/packages/vision-preset/src/editor.ts @@ -1,15 +1,18 @@ 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 { registerSetters } from '@ali/lowcode-setters'; -import Outline from '@ali/lowcode-plugin-outline-pane'; +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 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'; +import { Divider } from '@alifd/next'; export const editor = new Editor(); globalContext.register(editor, Editor); @@ -78,15 +81,15 @@ skeleton.add({ type: 'Panel', content: SettingsPrimaryPane, }); -skeleton.add({ - area: 'leftArea', - name: 'outlinePane', - type: 'PanelDock', - content: Outline, - panelProps: { - area: 'leftFixedArea', - }, -}); +// skeleton.add({ +// area: 'leftArea', +// name: 'outlinePane', +// type: 'PanelDock', +// content: Outline, +// panelProps: { +// area: 'leftFixedArea', +// }, +// }); skeleton.add({ area: 'topArea', @@ -112,3 +115,10 @@ skeleton.add({ // }, // content: SourceEditor, // }); + +// 实例节点选择器,线框高亮 +addBuiltinComponentAction({ + name: 'instance-node-selector', + content: InstanceNodeSelector, + important: true, +}); 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/index.ts b/packages/vision-preset/src/index.ts index 2c4db40e9..b3eaa9b01 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,6 +22,8 @@ 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 './vision.less'; @@ -101,6 +104,10 @@ const VisualEngine = { Bundle, Pages, DragEngine, + Viewport, + Version, + Project, + logger, }; (window as any).VisualEngine = VisualEngine; @@ -144,6 +151,10 @@ export { Bundle, Pages, DragEngine, + Viewport, + Version, + Project, + logger, }; diff --git a/packages/vision-preset/src/pages.ts b/packages/vision-preset/src/pages.ts index 81430a4f1..ff17f214c 100644 --- a/packages/vision-preset/src/pages.ts +++ b/packages/vision-preset/src/pages.ts @@ -15,8 +15,8 @@ const pages = Object.assign(project, { 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/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/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();