diff --git a/packages/designer/auxilary/README.md b/packages/designer/auxilary-bak/README.md similarity index 100% rename from packages/designer/auxilary/README.md rename to packages/designer/auxilary-bak/README.md diff --git a/packages/designer/auxilary/embed-editor-toolbar.tsx b/packages/designer/auxilary-bak/embed-editor-toolbar.tsx similarity index 100% rename from packages/designer/auxilary/embed-editor-toolbar.tsx rename to packages/designer/auxilary-bak/embed-editor-toolbar.tsx diff --git a/packages/designer/auxilary/insertion.less b/packages/designer/auxilary-bak/insertion.less similarity index 100% rename from packages/designer/auxilary/insertion.less rename to packages/designer/auxilary-bak/insertion.less diff --git a/packages/designer/auxilary/insertion.tsx b/packages/designer/auxilary-bak/insertion.tsx similarity index 100% rename from packages/designer/auxilary/insertion.tsx rename to packages/designer/auxilary-bak/insertion.tsx diff --git a/packages/designer/auxilary/auxiliary.less b/packages/designer/auxilary/auxiliary.less deleted file mode 100644 index 0cd365d85..000000000 --- a/packages/designer/auxilary/auxiliary.less +++ /dev/null @@ -1,20 +0,0 @@ -.my-auxiliary { - pointer-events: none; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - overflow: visible; - z-index: 800; - .embed-editor-toolbar { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - > * { - pointer-events: all; - } - } -} diff --git a/packages/designer/auxilary/auxiliary.tsx b/packages/designer/auxilary/auxiliary.tsx deleted file mode 100644 index 99bfd28b9..000000000 --- a/packages/designer/auxilary/auxiliary.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { observer } from '@ali/recore'; -import { Component } from 'react'; -import { getCurrentDocument } from '../../globals'; -import './auxiliary.less'; -import { EdgingView } from './gliding'; -import { InsertionView } from './insertion'; -import { SelectingView } from './selecting'; -import EmbedEditorToolbar from './embed-editor-toolbar'; - -@observer -export class AuxiliaryView extends Component { - shouldComponentUpdate() { - return false; - } - - render() { - const doc = getCurrentDocument(); - if (!doc || !doc.ready) { - return null; - } - const { scrollX, scrollY, scale } = doc.viewport; - return ( -
- - - - -
- ); - } -} diff --git a/packages/designer/auxilary/gliding.less b/packages/designer/auxilary/gliding.less deleted file mode 100644 index 733ae746d..000000000 --- a/packages/designer/auxilary/gliding.less +++ /dev/null @@ -1,39 +0,0 @@ -.my-edging { - box-sizing: border-box; - pointer-events: none; - position: absolute; - top: 0; - left: 0; - border: 1px dashed var(--color-brand-light); - z-index: 1; - background: rgba(95, 240, 114, 0.04); - will-change: transform, width, height; - transition-property: transform, width, height; - transition-duration: 60ms; - transition-timing-function: linear; - overflow: visible; - >.title { - position: absolute; - color: var(--color-brand-light); - top: -20px; - left: 0; - font-weight: lighter; - } - - &.x-shadow { - border-color: rgba(138, 93, 226, 0.8); - background: rgba(138, 93, 226, 0.04); - - >.title { - color: rgba(138, 93, 226, 1.0); - } - } - - &.x-flow { - border-color: rgba(255, 99, 8, 0.8); - background: rgba(255, 99, 8, 0.04); - >.title { - color: rgb(255, 99, 8); - } - } -} diff --git a/packages/designer/auxilary/gliding.tsx b/packages/designer/auxilary/gliding.tsx deleted file mode 100644 index a6b063ddc..000000000 --- a/packages/designer/auxilary/gliding.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { observer } from '@recore/core-obx'; -import { Component } from 'react'; -import './edging.less'; - -@observer -export class GlidingView extends Component { - shouldComponentUpdate() { - return false; - } - - render() { - const node = edging.watching; - if (!node || !edging.enable || (current.selection && current.selection.has(node.id))) { - return null; - } - - // TODO: think of multi ReactInstance - // TODO: findDOMNode cause a render bug - const rect = node.document.computeRect(node); - if (!rect) { - return null; - } - - const { scale, scrollTarget } = node.document.viewport; - - const sx = scrollTarget!.left; - const sy = scrollTarget!.top; - - const style = { - width: rect.width * scale, - height: rect.height * scale, - transform: `translate(${(sx + rect.left) * scale}px, ${(sy + rect.top) * scale}px)`, - } as any; - - let className = 'my-edging'; - - // TODO: - // 1. thinkof icon - // 2. thinkof top|bottom|inner space - - return ( -
- {(node as any).title || node.tagName} -
- ); - } -} diff --git a/packages/designer/auxilary/offset-observer.ts b/packages/designer/auxilary/offset-observer.ts deleted file mode 100644 index 960674b47..000000000 --- a/packages/designer/auxilary/offset-observer.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { obx } from '@ali/recore'; -import { INode } from '../../document/node'; - -export default class OffsetObserver { - @obx.ref offsetTop = 0; - @obx.ref offsetLeft = 0; - @obx.ref offsetRight = 0; - @obx.ref offsetBottom = 0; - @obx.ref height = 0; - @obx.ref width = 0; - @obx.ref hasOffset = false; - @obx.ref left = 0; - @obx.ref top = 0; - @obx.ref right = 0; - @obx.ref bottom = 0; - - private pid: number | undefined; - - constructor(node: INode) { - const document = node.document; - const scrollTarget = document.viewport.scrollTarget!; - - let pid: number; - const compute = () => { - if (pid !== this.pid) { - return; - } - - const rect = document.computeRect(node); - if (!rect) { - this.hasOffset = false; - return; - } - this.hasOffset = true; - this.offsetLeft = rect.left + scrollTarget.left; - this.offsetRight = rect.right + scrollTarget.left; - this.offsetTop = rect.top + scrollTarget.top; - this.offsetBottom = rect.bottom + scrollTarget.top; - this.height = rect.height; - this.width = rect.width; - this.left = rect.left; - this.top = rect.top; - this.right = rect.right; - this.bottom = rect.bottom; - this.pid = pid = (window as any).requestIdleCallback(compute); - }; - - // try first - compute(); - // try second, ensure the dom mounted - this.pid = pid = (window as any).requestIdleCallback(compute); - } - - destroy() { - if (this.pid) { - (window as any).cancelIdleCallback(this.pid); - } - this.pid = undefined; - } -} diff --git a/packages/designer/auxilary/selecting.less b/packages/designer/auxilary/selecting.less deleted file mode 100644 index 924c52d8c..000000000 --- a/packages/designer/auxilary/selecting.less +++ /dev/null @@ -1,39 +0,0 @@ -.my-selecting { - pointer-events: none; - position: absolute; - top: 0; - left: 0; - border: 1px solid var(--color-brand-light); - z-index: 2; - overflow: visible; - >.title { - position: absolute; - color: var(--color-brand-light); - top: -20px; - left: 0; - font-weight: lighter; - } - &.dragging { - background: rgba(182, 178, 178, 0.8); - border: none; - pointer-events: all; - } - - &.x-shadow { - border-color: rgba(147, 112, 219, 1.0); - background: rgba(147, 112, 219, 0.04); - >.title { - color: rgba(147, 112, 219, 1.0); - } - &.highlight { - background: transparent; - } - } - - &.x-flow { - border-color: rgb(255, 99, 8); - >.title { - color: rgb(255, 99, 8); - } - } -} diff --git a/packages/designer/auxilary/selecting.tsx b/packages/designer/auxilary/selecting.tsx deleted file mode 100644 index b89c35c99..000000000 --- a/packages/designer/auxilary/selecting.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { observer } from '@ali/recore'; -import { Component, Fragment } from 'react'; -import classNames from 'classnames'; -import { INode, isElementNode, isConfettiNode, hasConditionFlow } from '../../document/node'; -import OffsetObserver from './offset-observer'; -import './selecting.less'; -import { isShadowNode, isShadowsContainer } from '../../document/node/shadow-node'; -import { isConditionFlow } from '../../document/node/condition-flow'; -import { current, dragon } from '../../globals'; - -@observer -export class SingleSelectingView extends Component<{ node: INode; highlight?: boolean }> { - private offsetObserver: OffsetObserver; - - constructor(props: { node: INode; highlight?: boolean }) { - super(props); - this.offsetObserver = new OffsetObserver(props.node); - } - - render() { - if (!this.offsetObserver.hasOffset) { - return null; - } - - const scale = this.props.node.document.viewport.scale; - const { width, height, offsetTop, offsetLeft } = this.offsetObserver; - - const style = { - width: width * scale, - height: height * scale, - transform: `translate3d(${offsetLeft * scale}px, ${offsetTop * scale}px, 0)`, - } as any; - - const { node, highlight } = this.props; - - const className = classNames('my-selecting', { - 'x-shadow': isShadowNode(node), - 'x-flow': hasConditionFlow(node) || isConditionFlow(node), - highlight, - }); - - return
; - } -} - -@observer -export class SelectingView extends Component { - get selecting(): INode[] { - const sel = current.selection; - if (!sel) { - return []; - } - if (dragon.dragging) { - return sel.getTopNodes(); - } - - return sel.getNodes(); - } - render() { - return this.selecting.map(node => { - // select all nodes when doing x-for - if (isShadowsContainer(node)) { - // FIXME: thinkof nesting for - const views = []; - for (const shadowNode of (node as any).getShadows()!.values()) { - views.push(); - } - return {views}; - } else if (isShadowNode(node)) { - const shadows = node.origin.getShadows()!.values(); - const views = []; - for (const shadowNode of shadows) { - views.push(); - } - return {views}; - } - // select the visible node when doing x-if - else if (isConditionFlow(node)) { - return ; - } - - return ; - }); - } -} diff --git a/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.less b/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.less new file mode 100644 index 000000000..af0b195bc --- /dev/null +++ b/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.less @@ -0,0 +1,10 @@ +.lc-auxiliary { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + overflow: visible; + z-index: 800; +} diff --git a/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.tsx b/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.tsx new file mode 100644 index 000000000..ea5dbfcfd --- /dev/null +++ b/packages/designer/src/builtins/simulator/host/auxilary/auxiliary.tsx @@ -0,0 +1,28 @@ +import { observer } from '@recore/core-obx'; +import { Component } from 'react'; +import { OutlineHovering } from './outline-hovering'; +import { SimulatorContext } from '../context'; +import { SimulatorHost } from '../host'; +import './auxiliary.less'; +import './outlines.less'; +import { OutlineSelecting } from './outline-selecting'; + +@observer +export class AuxiliaryView extends Component { + static contextType = SimulatorContext; + + shouldComponentUpdate() { + return false; + } + + render() { + const host = this.context as SimulatorHost; + const { scrollX, scrollY, scale } = host.viewport; + return ( +
+ + +
+ ); + } +} diff --git a/packages/designer/auxilary/index.ts b/packages/designer/src/builtins/simulator/host/auxilary/index.ts similarity index 100% rename from packages/designer/auxilary/index.ts rename to packages/designer/src/builtins/simulator/host/auxilary/index.ts diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx b/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx new file mode 100644 index 000000000..a56109e1f --- /dev/null +++ b/packages/designer/src/builtins/simulator/host/auxilary/outline-hovering.tsx @@ -0,0 +1,110 @@ +import { Component, Fragment, PureComponent } from 'react'; +import classNames from 'classnames'; +import { observer } from '@recore/core-obx'; +import { SimulatorContext } from '../context'; +import { SimulatorHost } from '../host'; +import { computed } from '@recore/obx'; + +export class OutlineHoveringInstance extends PureComponent<{ + title: string; + rect: DOMRect | null; + scale: number; + scrollX: number; + scrollY: number; +}> { + render() { + const { title, rect, scale, scrollX, scrollY } = this.props; + if (!rect) { + return null; + } + + const style = { + width: rect.width * scale, + height: rect.height * scale, + transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`, + }; + + const className = classNames('lc-outlines lc-outlines-hovering'); + + // TODO: + // 1. thinkof icon + // 2. thinkof top|bottom|inner space + + return ( +
+ {title} +
+ ); + } +} + +@observer +export class OutlineHovering extends Component { + static contextType = SimulatorContext; + + shouldComponentUpdate() { + return false; + } + + @computed get scale() { + return (this.context as SimulatorHost).viewport.scale; + } + + @computed get scrollX() { + return (this.context as SimulatorHost).viewport.scrollX; + } + + @computed get scrollY() { + return (this.context as SimulatorHost).viewport.scrollY; + } + + @computed get current() { + const host = this.context as SimulatorHost; + const doc = host.document; + const selection = doc.selection; + const current = host.designer.hovering.current; + if (!current || current.document !== doc || selection.has(current.id)) { + return null; + } + return current; + } + + render() { + const host = this.context as SimulatorHost; + const current = this.current; + if (!current) { + return ; + } + const instances = host.getComponentInstance(current); + if (!instances || instances.length < 1) { + return ; + } + + if (instances.length === 1) { + return ( + + ); + } + return ( + + {instances.map((inst, i) => ( + + ))} + + ); + } +} diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx b/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx new file mode 100644 index 000000000..3b8dfd6fb --- /dev/null +++ b/packages/designer/src/builtins/simulator/host/auxilary/outline-selecting.tsx @@ -0,0 +1,95 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@recore/core-obx'; +import { SimulatorContext } from '../context'; +import { SimulatorHost } from '../host'; +import { computed } from '@recore/obx'; +import OffsetObserver from '../../../../designer/offset-observer'; + +@observer +export class OutlineSelectingInstance extends Component<{ observed: OffsetObserver; highlight?: boolean }> { + shouldComponentUpdate() { + return false; + } + + render() { + const { observed, highlight } = this.props; + if (!observed.hasOffset) { + return null; + } + + const { scale, width, height, offsetTop, offsetLeft } = observed; + + const style = { + width: width * scale, + height: height * scale, + transform: `translate3d(${offsetLeft * scale}px, ${offsetTop * scale}px, 0)`, + }; + + const className = classNames('lc-outlines lc-outlines-selecting', { + highlight, + }); + + return ( + + ); + } +} + +@observer +export class OutlineSelecting extends Component { + static contextType = SimulatorContext; + + shouldComponentUpdate() { + return false; + } + + @computed get selecting() { + const doc = this.host.document; + if (doc.suspensed) { + return null; + } + return doc.selection.getNodes(); + } + + @computed get host(): SimulatorHost { + return this.context; + } + + render() { + const selecting = this.selecting; + if (!selecting || selecting.length < 1) { + // DIRTY FIX, recore has a bug! + return ; + } + + const designer = this.host.designer; + + return ( + + {selecting.map(node => { + const instances = this.host.getComponentInstance(node); + if (!instances || instances.length < 1) { + return null; + } + return ( + + {instances.map((instance, i) => { + const observed = designer.createOffsetObserver({ + node, + instance, + }); + if (!observed) { + return null; + } + return ; + })} + + ); + })} + + ); + } +} diff --git a/packages/designer/src/builtins/simulator/host/auxilary/outlines.less b/packages/designer/src/builtins/simulator/host/auxilary/outlines.less new file mode 100644 index 000000000..fc3e1042e --- /dev/null +++ b/packages/designer/src/builtins/simulator/host/auxilary/outlines.less @@ -0,0 +1,73 @@ +@scope: lc-outlines; + +.@{scope} { + box-sizing: border-box; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + z-index: 1; + border: 1px solid var(--color-brand-light); + will-change: transform, width, height; + overflow: visible; + & > &-title { + position: absolute; + color: var(--color-brand-light); + top: 0; + left: 0; + transform: translateY(-100%); + font-weight: lighter; + } + + &&-hovering { + z-index: 1; + border-style: dashed; + background: rgba(95, 240, 114, 0.04); + + &.x-loop { + border-color: rgba(138, 93, 226, 0.8); + background: rgba(138, 93, 226, 0.04); + + >.@{scope}-title { + color: rgba(138, 93, 226, 1.0); + } + } + + &.x-condition { + border-color: rgba(255, 99, 8, 0.8); + background: rgba(255, 99, 8, 0.04); + >.@{scope}-title { + color: rgb(255, 99, 8); + } + } + } + + &&-selecting { + z-index: 2; + + &.x-loop { + border-color: rgba(147, 112, 219, 1.0); + background: rgba(147, 112, 219, 0.04); + + >@{scope}-title { + color: rgba(147, 112, 219, 1.0); + } + &.highlight { + background: transparent; + } + } + + &.x-condition { + border-color: rgb(255, 99, 8); + >@{scope}-title { + color: rgb(255, 99, 8); + } + } + + &.dragging { + background: rgba(182, 178, 178, 0.8); + border: none; + pointer-events: all; + } + } +} diff --git a/packages/designer/src/builtins/simulator/host/context.ts b/packages/designer/src/builtins/simulator/host/context.ts new file mode 100644 index 000000000..6532c3d86 --- /dev/null +++ b/packages/designer/src/builtins/simulator/host/context.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import { SimulatorHost } from './host'; + +export const SimulatorContext = createContext({} as any); diff --git a/packages/designer/src/builtins/simulator/host/host-view.tsx b/packages/designer/src/builtins/simulator/host/host-view.tsx index 9d42063df..bdc3168c4 100644 --- a/packages/designer/src/builtins/simulator/host/host-view.tsx +++ b/packages/designer/src/builtins/simulator/host/host-view.tsx @@ -1,8 +1,9 @@ -import { Component, createContext } from 'react'; +import { Component } from 'react'; import { observer } from '@recore/core-obx'; -// import { AuxiliaryView } from '../auxilary'; import { SimulatorHost, SimulatorProps } from './host'; import DocumentModel from '../../../designer/document/document-model'; +import { SimulatorContext } from './context'; +import { AuxiliaryView } from './auxilary'; import './host.less'; /* @@ -14,8 +15,6 @@ import './host.less'; Auxiliary 辅助显示层,初始相对 Content 位置 0,0,紧贴 Canvas, 根据 Content 滚动位置,改变相对位置 */ -export const SimulatorContext = createContext({} as any); - export class SimulatorHostView extends Component void; @@ -32,6 +31,7 @@ export class SimulatorHostView extends Component
sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport"> - {/**/} +
diff --git a/packages/designer/src/builtins/simulator/host/host.ts b/packages/designer/src/builtins/simulator/host/host.ts index e08408313..0127adcbb 100644 --- a/packages/designer/src/builtins/simulator/host/host.ts +++ b/packages/designer/src/builtins/simulator/host/host.ts @@ -11,11 +11,12 @@ import { DragObjectType, isShaken, LocateEvent, DragNodeObject, DragNodeDataObje import { LocationData } from '../../../designer/location'; import { NodeData } from '../../../designer/schema'; import { ComponentDescriptionSpec } from '../../../designer/document/node/component-config'; +import { ReactInstance } from 'react'; export interface SimulatorProps { // 从 documentModel 上获取 // suspended?: boolean; - designMode?: 'live' | 'design' | 'extend' | 'border' | 'preview'; + designMode?: 'live' | 'design' | 'mock' | 'extend' | 'border' | 'preview'; device?: 'mobile' | 'iphone' | string; deviceClassName?: string; simulatorUrl?: Asset; @@ -44,6 +45,7 @@ const defaultDepends = [ AssetType.JSText, 'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;', ), + assetItem(AssetType.JSUrl, 'https://g.alicdn.com/mylib/@ali/recore/1.5.7/umd/recore.min.js'), assetItem(AssetType.JSUrl, 'http://localhost:4444/js/index.js'), ]; @@ -206,7 +208,7 @@ export class SimulatorHost implements ISimulator { if (isMulti) { // multi select mode, directily add if (!selection.has(node.id)) { - // activeTracker.track(node); + designer.activeTracker.track(node); selection.add(node.id); ignoreUpSelected = true; } @@ -265,7 +267,8 @@ export class SimulatorHost implements ISimulator { return; } const node = this.document.getNodeFromElement(e.target as Element); - hovering.hover(node, e); + // TODO: enhance only hover one instance + hovering.hover(node); e.stopPropagation(); }; const leave = () => hovering.leave(this.document); @@ -330,8 +333,12 @@ export class SimulatorHost implements ISimulator { throw new Error('Method not implemented.'); } - getComponentInstance(node: Node): ComponentInstance[] | null { - throw new Error('Method not implemented.'); + getComponentInstance(node: Node): ReactInstance[] | null { + return this._renderer?.getComponentInstance(node.id) || null; + } + + getComponentInstanceId(instance: ReactInstance) { + } getComponentContext(node: Node): object { @@ -342,8 +349,59 @@ export class SimulatorHost implements ISimulator { return this.renderer?.getClosestNodeId(elem) || null; } - findDOMNodes(instance: ComponentInstance): (Element | Text)[] | null { - throw new Error('Method not implemented.'); + computeComponentInstanceRect(instance: ReactInstance): DOMRect | null { + const renderer = this.renderer!; + const elements = renderer.findDOMNodes(instance); + if (!elements) { + return null; + } + + let rects: DOMRect[] | undefined; + let last: { x: number; y: number; r: number; b: number; } | undefined; + while (true) { + if (!rects || rects.length < 1) { + const elem = elements.pop(); + if (!elem) { + break; + } + rects = renderer.getClientRects(elem); + } + const rect = rects.pop(); + if (!rect) { + break; + } + if (!last) { + last = { + x: rect.left, + y: rect.top, + r: rect.right, + b: rect.bottom, + }; + continue; + } + if (rect.left < last.x) { + last.x = rect.left; + } + if (rect.top < last.y) { + last.y = rect.top; + } + if (rect.right > last.r) { + last.r = rect.right; + } + if (rect.bottom > last.b) { + last.b = rect.bottom; + } + } + + if (last) { + return new DOMRect(last.x, last.y, last.r - last.x, last.b - last.y); + } + + return null; + } + + findDOMNodes(instance: ReactInstance): Array | null { + return this._renderer?.findDOMNodes(instance) || null; } private tryScrollAgain: number | null = null; diff --git a/packages/designer/src/builtins/simulator/renderer/renderer.ts b/packages/designer/src/builtins/simulator/renderer/renderer.ts index 9dfb6a3ac..c4c67159b 100644 --- a/packages/designer/src/builtins/simulator/renderer/renderer.ts +++ b/packages/designer/src/builtins/simulator/renderer/renderer.ts @@ -4,10 +4,11 @@ import { host } from './host'; import SimulatorRendererView from './renderer-view'; import { computed, obx } from '@recore/obx'; import { RootSchema, NpmInfo } from '../../../designer/schema'; -import { isElement } from '../../../utils/dom'; +import { isElement, getClientRects } from '../../../utils/dom'; import { Asset } from '../utils/asset'; import loader from '../utils/loader'; import { ComponentDescriptionSpec } from '../../../designer/document/node/component-config'; +import { findDOMNodes } from '../utils/react'; let REACT_KEY = ''; function cacheReactKey(el: Element): Element { @@ -193,6 +194,14 @@ export class SimulatorRenderer { return getClosestNodeId(element); } + findDOMNodes(instance: ReactInstance): Array | null { + return findDOMNodes(instance); + } + + getClientRects(element: Element | Text) { + return getClientRects(element); + } + private _running: boolean = false; run() { if (this._running) { diff --git a/packages/designer/src/designer/designer.less b/packages/designer/src/designer/designer.less index c36bc5eab..2c0631786 100644 --- a/packages/designer/src/designer/designer.less +++ b/packages/designer/src/designer/designer.less @@ -1,7 +1,52 @@ +@import 'variables.less'; + .lc-designer { + + --font-family: @font-family; + --font-size-label: @fontSize-4; + --font-size-text: @fontSize-5; + --font-size-btn-large: @fontSize-3; + --font-size-btn-medium: @fontSize-4; + --font-size-btn-small: @fontSize-5; + + --color-brand-light: rgb(102, 188, 92); + --color-icon: rgba(255, 255, 255, 0.8); + --color-visited: rgba(179, 182, 201, 0.4); + --color-actived: #498ee6; + + --color-border: @white-alpha-7; + --color-btn: #0079F2; + --color-btn-border: rgba(0, 121, 242, 0.3); + --color-btn-bg: #212938; + + --color-form-bg: #272A35; + --color-form-border: rgba(63,70,93,1); + + --color-text: @white-alpha-3; + --color-text-light: @white-alpha-1; + --color-field-placeholder: @white-alpha-5; + --color-pane-label: rgba(255, 255, 255, 0.9); + --color-border: rgba(63, 70, 93, 1); + --color-field-border: rgba(118, 137, 199, 0.6); + --color-function-warning: rgb(204, 131, 98); + --color-field-border-hover: rgb(118, 137, 199, 0.8); + --color-field-border-active: rgb(118, 137, 199); + --color-block-background-disabled: rgba(118, 137, 199, 0.35); + + --global-border-radius: @global-border-radius; + --input-border-radius: @input-border-radius; + --popup-border-radius: @popup-border-radius; + position: relative; + font-family: var(--font-family); + font-size: var(--font-size-text); min-width: 500px; min-height: 500px; + box-sizing: border-box; + + * { + box-sizing: border-box; + } .lc-project { position: absolute; top: 0; diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index b2ec25c2d..1ccdaf127 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -12,6 +12,8 @@ import Node, { insertChildren } from './document/node/node'; import { isRootNode } from './document/node/root-node'; import { ComponentDescriptionSpec, ComponentConfig } from './document/node/component-config'; import Scroller, { IScrollable } from './scroller'; +import { INodeInstance } from './simulator'; +import OffsetObserver, { createOffsetObserver } from './offset-observer'; export interface DesignerProps { className?: string; @@ -118,6 +120,10 @@ export default class Designer { return new Scroller(scrollable); } + createOffsetObserver(nodeInstance: INodeInstance): OffsetObserver | null { + return createOffsetObserver(nodeInstance); + } + /** * 获得合适的插入位置 */ diff --git a/packages/designer/src/designer/document/document-model.ts b/packages/designer/src/designer/document/document-model.ts index ee72f214c..3f9d7d64e 100644 --- a/packages/designer/src/designer/document/document-model.ts +++ b/packages/designer/src/designer/document/document-model.ts @@ -49,7 +49,7 @@ export default class DocumentModel { } constructor(readonly project: Project, schema: RootSchema) { - this.rootNode = new RootNode(this, schema); + this.rootNode = this.createNode(schema) as RootNode; this.id = this.rootNode.id; } diff --git a/packages/designer/src/designer/document/node/node.ts b/packages/designer/src/designer/document/node/node.ts index 81ca45831..f913b15eb 100644 --- a/packages/designer/src/designer/document/node/node.ts +++ b/packages/designer/src/designer/document/node/node.ts @@ -1,4 +1,4 @@ -import { obx } from '@recore/obx'; +import { obx, computed } from '@recore/obx'; import { NodeSchema, NodeData, PropsMap, PropsList } from '../../schema'; import Props from './props/props'; import DocumentModel from '../document-model'; @@ -24,10 +24,10 @@ const DIRECTIVES = ['condition', 'conditionGroup', 'loop', 'loopArgs', 'title', * condition * ------- future support ----- * conditionGroup - * title - * ignore - * locked - * hidden + * x-title + * x-ignore + * x-locked + * x-hidden */ export default class Node { /** @@ -84,6 +84,20 @@ export default class Node { return this._zLevel; } + @computed get title(): string { + let t = this.getDirective('x-title'); + if (!t && this.componentConfig.descriptor) { + t = this.getProp(this.componentConfig.descriptor, false); + } + if (t) { + const v = t.getAsString(); + if (v) { + return v; + } + } + return this.componentName; + } + constructor(readonly document: DocumentModel, nodeSchema: NodeSchema) { const { componentName, id, children, props, ...extras } = nodeSchema; this.id = id || `node$${document.nextId()}`; diff --git a/packages/designer/src/designer/document/node/props/prop.ts b/packages/designer/src/designer/document/node/props/prop.ts index 9986884d7..274c56d90 100644 --- a/packages/designer/src/designer/document/node/props/prop.ts +++ b/packages/designer/src/designer/document/node/props/prop.ts @@ -62,6 +62,13 @@ export default class Prop implements IPropParent { return null; } + @computed getAsString(): string { + if (this.type === 'literal') { + return this._value ? String(this._value) : ''; + } + return ''; + } + /** * set value, val should be JSON Object */ diff --git a/packages/designer/src/designer/hovering.ts b/packages/designer/src/designer/hovering.ts index 641b8e3a7..4c0cd0d9c 100644 --- a/packages/designer/src/designer/hovering.ts +++ b/packages/designer/src/designer/hovering.ts @@ -10,25 +10,24 @@ export default class Hovering { set enable(flag: boolean) { this._enable = flag; if (!flag) { - this._hovering = null; + this._current = null; } } @obx.ref xRayMode: boolean = false; - @obx.ref private _hovering: Node | null = null; - get hovering() { - return this._hovering; + @obx.ref private _current: Node | null = null; + get current() { + return this._current; } - @obx.ref event?: MouseEvent; - hover(node: Node | null, e: MouseEvent) { - this._hovering = node; - this.event = e; + hover(node: Node | null) { + console.info(node); + this._current = node; } leave(document: DocumentModel) { - if (this.hovering && this.hovering.document === document) { - this._hovering = null; + if (this.current && this.current.document === document) { + this._current = null; } } } diff --git a/packages/designer/src/designer/offset-observer.ts b/packages/designer/src/designer/offset-observer.ts new file mode 100644 index 000000000..7b29d572e --- /dev/null +++ b/packages/designer/src/designer/offset-observer.ts @@ -0,0 +1,75 @@ +import { obx, computed } from '@recore/obx'; +import { INodeInstance, IViewport } from './simulator'; +import Viewport from '../builtins/simulator/host/viewport'; + +export default class OffsetObserver { + @obx.ref hasOffset = false; + + @computed get offsetLeft() { + return this.left + this.viewport.scrollX; + } + @computed get offsetTop() { + return this.top + this.viewport.scrollY; + } + + @obx.ref height = 0; + @obx.ref width = 0; + @obx.ref left = 0; + @obx.ref top = 0; + + @computed get scale() { + return this.viewport.scale; + } + + private pid: number | undefined; + private viewport: IViewport; + + constructor(readonly nodeInstance: INodeInstance) { + const { node, instance } = nodeInstance; + const doc = node.document; + const host = doc.simulator!; + this.viewport = host.viewport; + if (!instance) { + return; + } + + let pid: number; + const compute = () => { + if (pid !== this.pid) { + return; + } + + const rect = host.computeComponentInstanceRect(instance!); + + if (!rect) { + this.hasOffset = false; + } else { + this.hasOffset = true; + this.height = rect.height; + this.width = rect.width; + this.left = rect.left; + this.top = rect.top; + } + this.pid = pid = (window as any).requestIdleCallback(compute); + }; + + // try first + compute(); + // try second, ensure the dom mounted + this.pid = pid = (window as any).requestIdleCallback(compute); + } + + destroy() { + if (this.pid) { + (window as any).cancelIdleCallback(this.pid); + } + this.pid = undefined; + } +} + +export function createOffsetObserver(nodeInstance: INodeInstance): OffsetObserver | null { + if (!nodeInstance.instance) { + return null; + } + return new OffsetObserver(nodeInstance); +} diff --git a/packages/designer/src/designer/simulator.ts b/packages/designer/src/designer/simulator.ts index 5d036c458..07dfc5635 100644 --- a/packages/designer/src/designer/simulator.ts +++ b/packages/designer/src/designer/simulator.ts @@ -136,6 +136,8 @@ export interface ISimulator

extends ISensor { getClosestNodeId(elem: Element): string | null; + computeComponentInstanceRect(instance: ComponentInstance): DOMRect | null; + findDOMNodes(instance: ComponentInstance): Array | null; setSuspense(suspensed: boolean): void; @@ -154,3 +156,8 @@ export type Component = ComponentType | object; * 组件实例定义 */ export type ComponentInstance = Element | ReactComponent | object; + +export interface INodeInstance { + node: Node; + instance?: ComponentInstance; +} diff --git a/packages/designer/src/designer/variables.less b/packages/designer/src/designer/variables.less new file mode 100644 index 000000000..f61f2341c --- /dev/null +++ b/packages/designer/src/designer/variables.less @@ -0,0 +1,170 @@ +/* + * 基础的 DPL 定义使用了 kuma base 的定义,参考: + * https://github.com/uxcore/kuma-base/tree/master/variables + */ + +/** + * =========================================================== + * ==================== Font Family ========================== + * =========================================================== + */ + +/* + * @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif; + */ + + @font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif; + @font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif; + + /** + * =========================================================== + * ===================== Color DPL =========================== + * =========================================================== + */ + + @brand-color-1: rgba(0, 108, 255, 1); + @brand-color-2: rgba(25, 122, 255, 1); + @brand-color-3: rgba(0, 96, 229, 1); + + @brand-color-1-3: rgba(0, 108, 255, 0.6); + @brand-color-1-4: rgba(0, 108, 255, 0.4); + @brand-color-1-5: rgba(0, 108, 255, 0.3); + @brand-color-1-6: rgba(0, 108, 255, 0.2); + @brand-color-1-7: rgba(0, 108, 255, 0.1); + + @brand-color: @brand-color-1; + + @white-alpha-1: rgb(255, 255, 255); // W-1 + @white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80 + @white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60 + @white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40 + @white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30 + @white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20 + @white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10 + @white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6 + + @dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100 + @dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80 + @dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60 + @dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40 + @dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30 + @dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20 + @dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10 + @dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6 + @dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4 + + @normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100 + @normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80 + @normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60 + @normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40 + @normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30 + @normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20 + @normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10 + @normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6 + @normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4 + + @normal-3: #77879c; + @normal-4: #a3aebd; + @normal-5: #bac3cc; + @normal-6: #d1d7de; + + @gray-dark: #333; // N2_4 + @gray: #666; // N2_3 + @gray-light: #999; // N2_2 + @gray-lighter: #ccc; // N2_1 + + @brand-secondary: #2c2f33; // B2_3 + // 补色 + @brand-complement: #00b3e8; // B3_1 + // 复合 + @brand-comosite: #00c587; // B3_2 + // 浓度 + @brand-deep: #73461d; // B3_3 + + // F1-1 + @brand-danger: rgb(240, 70, 49); + // F1-2 (10% white) + @brand-danger-hover: rgba(240, 70, 49, 0.9); + // F1-3 (5% black) + @brand-danger-focus: rgba(240, 70, 49, 0.95); + + // F2-1 + @brand-warning: rgb(250, 189, 14); + // F3-1 + @brand-success: rgb(102, 188, 92); + // F4-1 + @brand-link: rgb(102, 188, 92); + // F4-2 + @brand-link-hover: #2e76a6; + + // F1-1-7 A10 + @brand-danger-alpha-7: rgba(240, 70, 49, 0.9); + // F1-1-8 A6 + @brand-danger-alpha-8: rgba(240, 70, 49, 0.8); + // F2-1-2 A80 + @brand-warning-alpha-2: rgba(250, 189, 14, 0.8); + // F2-1-7 A10 + @brand-warning-alpha-7: rgba(250, 189, 14, 0.9); + // F3-1-2 A80 + @brand-success-alpha-2: rgba(102, 188, 92, 0.8); + // F3-1-7 A10 + @brand-success-alpha-7: rgba(102, 188, 92, 0.9); + // F4-1-7 A10 + @brand-link-alpha-7: rgba(102, 188, 92, 0.9); + + // 文本色 + @text-primary-color: @dark-alpha-3; + @text-secondary-color: @normal-alpha-3; + @text-thirdary-color: @dark-alpha-4; + @text-disabled-color: @normal-alpha-5; + @text-helper-color: @dark-alpha-4; + @text-danger-color: @brand-danger; + @text-ali-color: #ec6c00; + + /** + * =========================================================== + * =================== Shadow Box ============================ + * =========================================================== + */ + + @box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密 + @box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高 + @box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用 + + /** + * =========================================================== + * ================= FontSize of Level ======================= + * =========================================================== + */ + + @fontSize-1: 26px; + @fontSize-2: 20px; + @fontSize-3: 16px; + @fontSize-4: 14px; + @fontSize-5: 12px; + + @fontLineHeight-1: 38px; + @fontLineHeight-2: 30px; + @fontLineHeight-3: 26px; + @fontLineHeight-4: 24px; + @fontLineHeight-5: 20px; + + /** + * =========================================================== + * ================= FontSize of Level ======================= + * =========================================================== + */ + + @global-border-radius: 3px; + @input-border-radius: 3px; + @popup-border-radius: 6px; + + /** + * =========================================================== + * ===================== Transistion ========================= + * =========================================================== + */ + + @transition-duration: 0.3s; + @transition-ease: cubic-bezier(0.23, 1, 0.32, 1); + @transition-delay: 0s;