complete outline

This commit is contained in:
kangwei 2020-02-18 19:55:49 +08:00
parent 5f0ac69595
commit b849377de4
30 changed files with 739 additions and 350 deletions

View File

@ -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;
}
}
}

View File

@ -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 (
<div className="my-auxiliary" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
<EmbedEditorToolbar />
<EdgingView />
<InsertionView />
<SelectingView />
</div>
);
}
}

View File

@ -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);
}
}
}

View File

@ -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 (
<div className={className} style={style}>
<a className="title">{(node as any).title || node.tagName}</a>
</div>
);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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 <div className={className} style={style} />;
}
}
@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(<SingleSelectingView key={shadowNode.id} node={shadowNode} />);
}
return <Fragment key={node.id}>{views}</Fragment>;
} else if (isShadowNode(node)) {
const shadows = node.origin.getShadows()!.values();
const views = [];
for (const shadowNode of shadows) {
views.push(<SingleSelectingView highlight={shadowNode === node} key={shadowNode.id} node={shadowNode} />);
}
return <Fragment key={node.id}>{views}</Fragment>;
}
// select the visible node when doing x-if
else if (isConditionFlow(node)) {
return <SingleSelectingView node={node.visibleNode} key={node.id} />;
}
return <SingleSelectingView node={node} key={node.id} />;
});
}
}

View File

@ -0,0 +1,10 @@
.lc-auxiliary {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: visible;
z-index: 800;
}

View File

@ -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 (
<div className="lc-auxiliary" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
<OutlineHovering key="hovering" />
<OutlineSelecting key="selecting" />
</div>
);
}
}

View File

@ -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 (
<div className={className} style={style}>
<a className="lc-outlines-title">{title}</a>
</div>
);
}
}
@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 <Fragment />;
}
const instances = host.getComponentInstance(current);
if (!instances || instances.length < 1) {
return <Fragment />;
}
if (instances.length === 1) {
return (
<OutlineHoveringInstance
key="line-s"
title={current.title}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
rect={host.computeComponentInstanceRect(instances[0])}
/>
);
}
return (
<Fragment>
{instances.map((inst, i) => (
<OutlineHoveringInstance
key={`line-${i}`}
title={current.title}
scale={this.scale}
scrollX={this.scrollX}
scrollY={this.scrollY}
rect={host.computeComponentInstanceRect(inst)}
/>
))}
</Fragment>
);
}
}

View File

@ -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 (
<div className={className} style={style}>
<a className="lc-outlines-title">{observed.nodeInstance.node.title}</a>
</div>
);
}
}
@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 <Fragment />;
}
const designer = this.host.designer;
return (
<Fragment>
{selecting.map(node => {
const instances = this.host.getComponentInstance(node);
if (!instances || instances.length < 1) {
return null;
}
return (
<Fragment key={node.id}>
{instances.map((instance, i) => {
const observed = designer.createOffsetObserver({
node,
instance,
});
if (!observed) {
return null;
}
return <OutlineSelectingInstance key={`line-s-${i}`} observed={observed} />;
})}
</Fragment>
);
})}
</Fragment>
);
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,4 @@
import { createContext } from 'react';
import { SimulatorHost } from './host';
export const SimulatorContext = createContext<SimulatorHost>({} as any);

View File

@ -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<SimulatorHost>({} as any);
export class SimulatorHostView extends Component<SimulatorProps & {
documentContext: DocumentModel;
onMount?: (host: SimulatorHost) => void;
@ -32,6 +31,7 @@ export class SimulatorHostView extends Component<SimulatorProps & {
return false;
}
componentDidMount() {
console.info('mount simulator');
if (this.props.onMount) {
this.props.onMount(this.host);
}
@ -64,7 +64,7 @@ class Canvas extends Component {
return (
<div className={className}>
<div ref={elmt => sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport">
{/*<AuxiliaryView />*/}
<AuxiliaryView />
<Content />
</div>
</div>

View File

@ -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<SimulatorProps> {
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<SimulatorProps> {
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<SimulatorProps> {
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<SimulatorProps> {
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<Element | Text> | null {
return this._renderer?.findDOMNodes(instance) || null;
}
private tryScrollAgain: number | null = null;

View File

@ -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<Element | Text> | null {
return findDOMNodes(instance);
}
getClientRects(element: Element | Text) {
return getClientRects(element);
}
private _running: boolean = false;
run() {
if (this._running) {

View File

@ -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;

View File

@ -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);
}
/**
*
*/

View File

@ -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;
}

View File

@ -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()}`;

View File

@ -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
*/

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -136,6 +136,8 @@ export interface ISimulator<P = object> extends ISensor {
getClosestNodeId(elem: Element): string | null;
computeComponentInstanceRect(instance: ComponentInstance): DOMRect | null;
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null;
setSuspense(suspensed: boolean): void;
@ -154,3 +156,8 @@ export type Component = ComponentType<any> | object;
*
*/
export type ComponentInstance = Element | ReactComponent<any> | object;
export interface INodeInstance {
node: Node;
instance?: ComponentInstance;
}

View File

@ -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;