debug start

This commit is contained in:
kangwei 2020-02-18 01:28:01 +08:00
parent a12aa43c64
commit 28d23d5d78
70 changed files with 5509 additions and 181 deletions

View File

@ -0,0 +1,39 @@
.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

@ -0,0 +1,47 @@
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

@ -3,16 +3,13 @@
"version": "0.9.0",
"description": "alibaba lowcode designer",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"@ali/iceluna-sdk": "^1.0.5-beta.12",
"@recore/core-obx": "^1.0.4",
"@recore/obx": "^1.0.5",
"@types/medium-editor": "^5.0.3",
"@ali/lowcode-renderer": "0.9.0",
"classnames": "^2.2.6",
"react": "^16",
"react-dom": "^16.7.0"

View File

@ -0,0 +1,82 @@
import { Component } from 'react';
import { obx } from '@recore/obx';
import { observer } from '@recore/core-obx';
import Designer from '../../designer/designer';
import './ghost.less';
type offBinding = () => any;
@observer
export default class Ghost extends Component<{ designer: Designer }> {
private dispose: offBinding[] = [];
@obx.ref private dragment: any = null;
@obx.ref private x = 0;
@obx.ref private y = 0;
private dragon = this.props.designer.dragon;
componentWillMount() {
this.dispose = [
this.dragon.onDragstart((e) => {
this.dragment = e.dragObject;
this.x = e.globalX;
this.y = e.globalY;
}),
this.dragon.onDrag(e => {
this.x = e.globalX;
this.y = e.globalY;
}),
this.dragon.onDragend(() => {
this.dragment = null;
this.x = 0;
this.y = 0;
}),
];
}
shouldComponentUpdate() {
return false;
}
componentWillUnmount() {
if (this.dispose) {
this.dispose.forEach(off => off());
}
}
renderGhostGroup() {
const dragment = this.dragment;
if (Array.isArray(dragment)) {
return dragment.map((node: any, index: number) => {
const ghost = (
<div className="my-ghost" key={`ghost-${index}`}>
<div className="my-ghost-title">{node.tagName}</div>
</div>
);
return ghost;
});
} else {
return (
<div className="my-ghost">
<div className="my-ghost-title">{dragment.tagName}</div>
</div>
);
}
}
render() {
if (!this.dragment) {
return null;
}
return (
<div
className="my-ghost-group"
style={{
transform: `translate(${this.x}px, ${this.y}px)`,
}}
>
{this.renderGhostGroup()}
</div>
);
}
}

View File

@ -1,59 +0,0 @@
import MediumEditor from 'medium-editor';
import { computed, obx } from '@ali/recore';
import { current } from './current';
import ElementNode from '../document/node/element-node';
class EmbedEditor {
@obx container?: HTMLDivElement | null;
private _editor?: any;
@computed getEditor(): any | null {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
const win = current.document!.contentWindow;
const doc = current.document!.ownerDocument;
if (!win || !doc || !this.container) {
return null;
}
const rect = this.container.getBoundingClientRect();
this._editor = new MediumEditor([], {
contentWindow: win,
ownerDocument: doc,
toolbar: {
diffLeft: rect.left,
diffTop: rect.top - 10,
},
elementsContainer: this.container,
});
return this._editor;
}
@obx.ref editing?: [ElementNode, string, HTMLElement];
edit(node: ElementNode, prop: string, el: HTMLElement) {
const ed = this.getEditor();
if (!ed) {
return;
}
this.exitAndSave();
console.info(el);
this.editing = [node, prop, el];
ed.origElements = el;
ed.setup();
}
exitAndSave() {
this.editing = undefined;
// removeElements
// get content save to
}
mount(container?: HTMLDivElement | null) {
this.container = container;
}
}
export default new EmbedEditor();

View File

@ -0,0 +1 @@
主进程

View File

@ -0,0 +1,77 @@
// NOTE: 仅用作类型标注,切勿作为实体使用
import { SimulatorRenderer } from '../renderer/renderer';
import { SimulatorHost } from './host';
import { AssetLevel, AssetList, isAssetBundle, isAssetItem, isCSSUrl, AssetType, assetItem } from '../utils/asset';
export function createSimulator(host: SimulatorHost, iframe: HTMLIFrameElement, vendors: AssetList = []): Promise<SimulatorRenderer> {
const win: any = iframe.contentWindow;
const doc = iframe.contentDocument!;
win.LCSimulatorHost = host;
const styles: any = {};
const scripts: any = {};
Object.keys(AssetLevel).forEach((key) => {
const v = (AssetLevel as any)[key];
styles[v] = [];
scripts[v] = [];
});
function parseAssetList(assets: AssetList, level?: AssetLevel) {
for (let asset of assets) {
if (!asset) {
continue;
}
if (isAssetBundle(asset)) {
if (asset.assets) {
parseAssetList(Array.isArray(asset.assets) ? asset.assets : [asset.assets], asset.level || level);
}
continue;
}
if (Array.isArray(asset)) {
parseAssetList(asset, level);
continue;
}
if (!isAssetItem(asset)) {
asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!;
}
const id = asset.id ? ` data-id="${asset.id}"` : '';
const lv = asset.level || level || AssetLevel.BaseDepends;
if (asset.type === 'jsUrl') {
(scripts[lv] || scripts[AssetLevel.App]).push(`<script src="${asset.content}"${id}></script>`)
} else if (asset.type === 'jsText') {
(scripts[lv] || scripts[AssetLevel.App]).push(`<script${id}>${asset.content}</script>`);
} else if (asset.type === 'cssUrl') {
(styles[lv] || styles[AssetLevel.App]).push(`<link rel="stylesheet" href="${asset.content}"${id} />`);
} else if (asset.type === 'cssText') {
(styles[lv] || styles[AssetLevel.App]).push(`<style type="text/css"${id}>${asset.content}</style>`);
}
}
}
parseAssetList(vendors);
const styleFrags = Object.keys(styles).map(key => {
return styles[key].join('\n') + `<meta level="${key}" />`;
});
const scriptFrags = Object.keys(scripts).map(key => {
return styles[key].join('\n') + `<meta level="${key}" />`;
});
doc.open();
doc.write(`<!doctype html><html><head><meta charset="utf-8"/>
${styleFrags}
</head><body>${scriptFrags}</body></html>`);
doc.close();
return new Promise(resolve => {
if (win.SimulatorRenderer || host.renderer) {
return resolve(win.SimulatorRenderer || host.renderer);
}
const loaded = () => {
resolve(win.SimulatorRenderer || host.renderer);
win.removeEventListener('load', loaded);
};
win.addEventListener('load', loaded);
});
}

View File

@ -0,0 +1,95 @@
import { Component, createContext } 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 './host.less';
/*
Simulator , 使 Canvas Canvas
Canvas(DeviceShell) CanvasViewport
CanvasViewport Canvas
Content(Shell) CanvasViewport margin
ContentFrame Content
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;
}> {
readonly host: SimulatorHost;
constructor(props: any) {
super(props);
const { documentContext } = this.props;
this.host = (documentContext.simulator as SimulatorHost) || new SimulatorHost(documentContext);
}
shouldComponentUpdate(nextProps: SimulatorProps) {
this.host.setProps(nextProps);
return false;
}
componentDidMount() {
if (this.props.onMount) {
this.props.onMount(this.host);
}
}
render() {
const { Provider } = SimulatorContext;
return (
<div className="lc-simulator">
<Provider value={this.host}>
{/*progressing.visible ? <PreLoaderView /> : null*/}
<Canvas />
</Provider>
</div>
);
}
}
@observer
class Canvas extends Component {
static contextType = SimulatorContext;
render() {
const sim = this.context as SimulatorHost;
let className = 'lc-simulator-canvas';
if (sim.deviceClassName) {
className += ` ${sim.deviceClassName}`;
} else if (sim.device) {
className += ` lc-simulator-device-${sim.device}`;
}
return (
<div className={className}>
<div ref={elmt => sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport">
{/*<AuxiliaryView />*/}
<Content />
</div>
</div>
);
}
}
@observer
class Content extends Component {
static contextType = SimulatorContext;
render() {
const sim = this.context as SimulatorHost;
const viewport = sim.viewport;
let frameStyle = {};
if (viewport.scale < 1) {
frameStyle = {
transform: `scale(${viewport.scale})`,
height: viewport.contentHeight,
width: viewport.contentWidth,
};
}
return (
<div className="lc-simulator-content">
<iframe className="lc-simulator-content-frame" style={frameStyle} ref={frame => sim.mountContentFrame(frame)} />
</div>
);
}
}

View File

@ -0,0 +1,67 @@
@scope: lc-simulator;
.@{scope} {
position: relative;
height: 100%;
width: 100%;
&-canvas {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
&-viewport {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
}
}
&-device-mobile {
left: 50%;
width: 460px;
transform: translateX(-50%);
box-shadow: 0 2px 10px 0 rgba(31,56,88,.15);
}
&-device-iphone6 {
left: 50%;
width: 368px;
transform: translateX(-50%);
background: url(https://img.alicdn.com/tps/TB12GetLpXXXXXhXFXXXXXXXXXX-756-1544.png) no-repeat top;
background-size: 378px 772px;
top: 8px;
.@{scope}-canvas-viewport {
top: 114px;
left: 25px;
right: 25px;
max-height: 561px;
border-radius: 0 0 2px 2px;
}
}
&-device-legao {
margin: 15px;
box-shadow: 0 2px 10px 0 rgba(31,56,88,.15);
}
&-content {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
&-frame {
border: none;
transform-origin: 0 0;
height: 100%;
width: 100%;
}
}
}

View File

@ -0,0 +1,799 @@
import { obx, autorun, computed } from '@recore/obx';
import { ISimulator, ComponentInstance, Component } from '../../../designer/simulator';
import Viewport from './viewport';
import { createSimulator } from './create-simulator';
import { SimulatorRenderer } from '../renderer/renderer';
import Node, { NodeParent } from '../../../designer/document/node/node';
import DocumentModel from '../../../designer/document/document-model';
import ResourceConsumer from './resource-consumer';
import { AssetLevel, Asset, assetBundle } from '../utils/asset';
import { DragObjectType, isShaken, LocateEvent, DragNodeObject, DragNodeDataObject } from '../../../designer/dragon';
import { LocationData } from '../../../designer/location';
import { NodeData } from '../../../designer/schema';
import { ComponentDescriptionSpec } from '../../../designer/document/node/component-config';
export interface SimulatorProps {
// 从 documentModel 上获取
// suspended?: boolean;
designMode?: 'live' | 'design' | 'extend' | 'border' | 'preview';
device?: 'mobile' | 'iphone' | string;
deviceClassName?: string;
simulatorUrl?: Asset;
dependsAsset?: Asset;
themesAsset?: Asset;
componentsAsset?: Asset;
[key: string]: any;
}
const publicPath = (document.currentScript as HTMLScriptElement).src.replace(/^(.*\/)[^/]+$/, '$1');
const defaultSimulatorUrl = (() => {
let urls;
if (process.env.NODE_ENV === 'production') {
urls = [`${publicPath}simulator-renderer.min.css`, `${publicPath}simulator-renderer.min.js`];
} else {
urls = [`${publicPath}simulator-renderer.js`];
}
return urls;
})();
const defaultDepends = [
{
type: 'jsUrl',
content:
'https://g.alicdn.com/mylib/??react/16.11.0/umd/react.production.min.js,react-dom/16.8.6/umd/react-dom.production.min.js,prop-types/15.7.2/prop-types.min.js',
id: 'rect',
},
{
type: 'jsText',
content:
'React.PropTypes=window.PropTypes;window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;',
},
];
export class SimulatorHost implements ISimulator<SimulatorProps> {
constructor(readonly document: DocumentModel) {}
readonly designer = this.document.designer;
private _sensorAvailable: boolean = true;
get sensorAvailable(): boolean {
return this._sensorAvailable;
}
@computed get device(): string | undefined {
// 根据 device 不同来做画布外框样式变化 渲染时可选择不同组件
// renderer 依赖
return this.get('device');
}
@computed get deviceClassName(): string | undefined {
return this.get('deviceClassName');
}
@computed get designMode(): 'live' | 'design' | 'extend' | 'border' | 'preview' {
// renderer 依赖
// TODO: 需要根据 design mode 不同切换鼠标响应情况
return this.get('designMode') || 'design';
}
@computed get componentsAsset(): Asset | undefined {
return this.get('componentsAsset');
}
@computed get themesAsset(): Asset | undefined {
return this.get('themesAsset');
}
@computed get componentsMap() {
// renderer 依赖
return this.designer.componentsMap;
}
@obx.ref _props: SimulatorProps = {};
setProps(props: SimulatorProps) {
this._props = props;
}
set(key: string, value: any) {
this._props = {
...this._props,
[key]: value,
};
}
get(key: string): any {
return this._props[key];
}
/**
* Renderer
*/
connect(renderer: SimulatorRenderer, fn: (context: { dispose: () => void; firstRun: boolean }) => void) {
this._renderer = renderer;
return autorun(fn as any, true);
}
purge(): void {}
readonly viewport = new Viewport();
readonly scroller = this.designer.createScroller(this.viewport);
mountViewport(viewport: Element | null) {
if (!viewport) {
return;
}
this.viewport.mount(viewport);
}
@obx.ref private _contentWindow?: Window;
get contentWindow() {
return this._contentWindow;
}
@obx.ref private _contentDocument?: Document;
get contentDocument() {
return this._contentDocument;
}
private _renderer?: SimulatorRenderer;
get renderer() {
return this._renderer;
}
readonly componentsConsumer = new ResourceConsumer<{
componentsAsset?: Asset;
componentsMap: object;
}>(() => {
return {
componentsAsset: this.componentsAsset,
componentsMap: this.componentsMap,
};
});
readonly injectionConsumer = new ResourceConsumer(() => {
return {};
});
async mountContentFrame(iframe: HTMLIFrameElement | null) {
if (!iframe) {
return;
}
this._contentWindow = iframe.contentWindow!;
const vendors = [
// required & use once
assetBundle(this.get('dependsAsset') || defaultDepends, AssetLevel.BaseDepends),
// required & TODO: think of update
assetBundle(this.themesAsset, AssetLevel.Theme),
// required & use once
assetBundle(this.get('simulatorUrl') || defaultSimulatorUrl, AssetLevel.Runtime),
];
// wait 准备 iframe 内容、依赖库注入
const renderer = await createSimulator(this, iframe, vendors);
// wait 业务组件被第一次消费,否则会渲染出错
await this.componentsConsumer.waitFirstConsume();
// wait 运行时上下文
await this.injectionConsumer.waitFirstConsume();
// step 5 ready & render
renderer.run();
// init events, overlays
this._contentDocument = this._contentWindow.document;
this.viewport.setScrollTarget(this._contentWindow);
this.setupEvents();
// hotkey.mount(this.contentWindow);
// clipboard.injectCopyPaster(this.ownerDocument);
}
setupEvents() {
this.setupDragAndClick();
this.setupHovering();
}
setupDragAndClick() {
const documentModel = this.document;
const selection = documentModel.selection;
const designer = documentModel.designer;
const doc = this.contentDocument!;
// TODO: think of lock when edit a node
// 事件路由
doc.addEventListener('mousedown', (downEvent: MouseEvent) => {
const target = documentModel.getNodeFromElement(downEvent.target as Element);
if (!target) {
selection.clear();
return;
}
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
if (isLeftButton) {
let node: Node = target;
let nodes: Node[] = [node];
let ignoreUpSelected = false;
if (isMulti) {
// multi select mode, directily add
if (!selection.has(node.id)) {
// activeTracker.track(node);
selection.add(node.id);
ignoreUpSelected = true;
}
// 获得顶层 nodes
nodes = selection.getTopNodes();
} else if (selection.containsNode(target)) {
nodes = selection.getTopNodes();
} else {
// will clear current selection & select dragment in dragstart
}
designer.dragon.boost(
{
type: DragObjectType.Node,
nodes,
},
downEvent,
);
if (ignoreUpSelected) {
// multi select mode has add selected, should return
return;
}
}
const checkSelect = (e: MouseEvent) => {
doc.removeEventListener('mouseup', checkSelect, true);
if (!isShaken(downEvent, e)) {
// const node = hasConditionFlow(target) ? target.conditionFlow : target;
const node = target;
const id = node.id;
designer.activeTracker.track(node);
if (isMulti && selection.has(id)) {
selection.remove(id);
} else {
selection.select(id);
}
}
};
doc.addEventListener('mouseup', checkSelect, true);
});
// cause edit
doc.addEventListener('dblclick', (e: MouseEvent) => {
// TODO:
});
}
private disableHovering?: () => void;
/**
*
*/
setupHovering() {
const doc = this.contentDocument!;
const hovering = this.document.designer.hovering;
const hover = (e: MouseEvent) => {
if (!hovering.enable) {
return;
}
const node = this.document.getNodeFromElement(e.target as Element);
hovering.hover(node, e);
e.stopPropagation();
};
const leave = () => hovering.leave(this.document);
doc.addEventListener('mouseover', hover, true);
doc.addEventListener('mouseleave', leave, false);
// TODO: refactor this line, contains click, mousedown, mousemove
doc.addEventListener(
'mousemove',
(e: Event) => {
e.stopPropagation();
},
true,
);
this.disableHovering = () => {
hovering.leave(this.document);
doc.removeEventListener('mouseover', hover, true);
doc.removeEventListener('mouseleave', leave, false);
this.disableHovering = undefined;
};
}
setDraggingState(state: boolean): void {
throw new Error('Method not implemented.');
}
isDraggingState(): boolean {
throw new Error('Method not implemented.');
}
setCopyState(state: boolean): void {
throw new Error('Method not implemented.');
}
setSuspense(suspended: boolean) {
if (suspended) {
if (this.disableHovering) {
this.disableHovering();
}
// sleep some autorun reaction
} else {
// weekup some autorun reaction
if (!this.disableHovering) {
this.setupHovering();
}
}
}
setDesignMode(mode: string): void {
throw new Error('Method not implemented.');
}
isCopyState(): boolean {
throw new Error('Method not implemented.');
}
clearState(): void {
throw new Error('Method not implemented.');
}
describeComponent(component: Component): ComponentDescriptionSpec {
throw new Error('Method not implemented.');
}
getComponent(componentName: string): Component {
throw new Error('Method not implemented.');
}
getComponentInstance(node: Node): ComponentInstance[] | null {
throw new Error('Method not implemented.');
}
getComponentContext(node: Node): object {
throw new Error('Method not implemented.');
}
getClosestNodeId(elem: Element): string {
throw new Error('Method not implemented.');
}
findDOMNodes(instance: ComponentInstance): (Element | Text)[] | null {
throw new Error('Method not implemented.');
}
private tryScrollAgain: number | null = null;
scrollToNode(node: Node, detail?: any, tryTimes = 0) {
this.tryScrollAgain = null;
if (this.sensing) {
// actived sensor
return;
}
const opt: any = {};
let scroll = false;
if (detail) {
// TODO:
/*
const rect = insertion ? insertion.getNearRect() : node.getRect();
let y;
let scroll = false;
if (insertion && rect) {
y = insertion.isNearAfter() ? rect.bottom : rect.top;
if (y < bounds.top || y > bounds.bottom) {
scroll = true;
}
}*/
} else {
/*
const rect = this.document.computeRect(node);
if (!rect || rect.width === 0 || rect.height === 0) {
if (!this.tryScrollAgain && tryTimes < 3) {
this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(node, null, tryTimes + 1));
}
return;
}
const scrollTarget = this.viewport.scrollTarget!;
const st = scrollTarget.top;
const sl = scrollTarget.left;
const { scrollHeight, scrollWidth } = scrollTarget;
const { height, width, top, bottom, left, right } = this.viewport.contentBounds;
if (rect.height > height ? rect.top > bottom || rect.bottom < top : rect.top < top || rect.bottom > bottom) {
opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height);
scroll = true;
}
if (rect.width > width ? rect.left > right || rect.right < left : rect.left < left || rect.right > right) {
opt.left = Math.min(rect.left + rect.width / 2 + sl - left - width / 2, scrollWidth - width);
scroll = true;
}*/
}
if (scroll && this.scroller) {
this.scroller.scrollTo(opt);
}
}
fixEvent(e: LocateEvent): LocateEvent {
/*
if (e.fixed) {
return e;
}
if (!e.target || e.originalEvent.view!.document !== this.contentDocument) {
e.target = this.contentDocument!.elementFromPoint(e.canvasX, e.canvasY);
}*/
return e;
}
isEnter(e: LocateEvent): boolean {
return false; /*
const rect = this.bounds;
return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right;
*/
}
private sensing: boolean = false;
deactiveSensor() {
this.sensing = false;
this.scroller.cancel();
}
//#region drag locate logic
getDropTarget(e: LocateEvent): NodeParent | LocationData | null {
/*
const { target, dragTarget } = e;
const isAny = isAnyDragTarget(dragTarget);
let container: any;
if (target) {
const ref = this.document.getNodeFromElement(target as Element);
if (ref) {
container = ref;
} else if (isAny) {
return null;
} else {
container = this.document.view;
}
} else if (isAny) {
return null;
} else {
container = this.document.view;
}
if (!isElementNode(container) && !isRootNode(container)) {
container = container.parent;
}
// use spec container to accept specialData
if (isAny) {
while (container) {
if (isRootNode(container)) {
return null;
}
const locationData = this.acceptAnyData(container, e);
if (locationData) {
return locationData;
}
container = container.parent;
}
return null;
}
let res: any;
let upward: any;
// TODO: improve AT_CHILD logic, mark has checked
while (container) {
res = this.acceptNodes(container, e);
if (isLocationData(res)) {
return res;
}
if (res === true) {
return container;
}
if (!res) {
if (upward) {
container = upward;
upward = null;
} else {
container = container.parent;
}
} else if (res === AT_CHILD) {
if (!upward) {
upward = container.parent;
}
container = this.getNearByContainer(container, e);
if (!container) {
container = upward;
upward = null;
}
} else if (isNode(res)) {
container = res;
upward = null;
}
}*/
return null;
}
acceptNodes(container: Node, e: LocateEvent) {
/*
const { dragTarget } = e;
if (isRootNode(container)) {
return this.checkDropTarget(container, dragTarget as any);
}
const proto = container.prototype;
const acceptable: boolean = this.isAcceptable(container);
if (!proto.isContainer && !acceptable) {
return false;
}
// check is contains, get common parent
if (isNodesDragTarget(dragTarget)) {
const nodes = dragTarget.nodes;
let i = nodes.length;
let p: any = container;
while (i-- > 0) {
if (contains(nodes[i], p)) {
p = nodes[i].parent;
}
}
if (p !== container) {
return p || this.document.view;
}
}
// first use accept
if (acceptable) {
const view: any = this.document.getView(container);
if (view && '$accept' in view) {
if (view.$accept === false) {
return false;
}
if (view.$accept === AT_CHILD || view.$accept === '@CHILD') {
return AT_CHILD;
}
if (typeof view.$accept === 'function') {
const ret = view.$accept(container, e);
if (ret || ret === false) {
return ret;
}
}
}
if (proto.acceptable) {
const ret = proto.accept(container, e);
if (ret || ret === false) {
return ret;
}
}
}
return this.checkNesting(container, dragTarget as any);
*/
}
getNearByContainer(container: NodeParent, e: LocateEvent) {
/*
const children = container.children;
if (!children || children.length < 1) {
return null;
}
let nearDistance: any = null;
let nearBy: any = null;
for (let i = 0, l = children.length; i < l; i++) {
let child: any = children[i];
if (!isElementNode(child)) {
continue;
}
if (hasConditionFlow(child)) {
const bn = child.conditionFlow;
i = bn.index + bn.length - 1;
child = bn.visibleNode;
}
const rect = this.document.computeRect(child);
if (!rect) {
continue;
}
if (isPointInRect(e, rect)) {
return child;
}
const distance = distanceToRect(e, rect);
if (nearDistance === null || distance < nearDistance) {
nearDistance = distance;
nearBy = child;
}
}
return nearBy;*/
}
locate(e: LocateEvent): any {
/*
this.sensing = true;
this.scroller.scrolling(e);
const dropTarget = this.getDropTarget(e);
if (!dropTarget) {
return null;
}
if (isLocationData(dropTarget)) {
return this.document.createLocation(dropTarget);
}
const target = dropTarget;
const edge = this.document.computeRect(target);
const children = target.children;
const detail: LocationChildrenDetail = {
type: LocationDetailType.Children,
index: 0,
};
const locationData = {
target,
detail,
};
if (!children || children.length < 1 || !edge) {
return this.document.createLocation(locationData);
}
let nearRect = null;
let nearIndex = 0;
let nearNode = null;
let nearDistance = null;
let top = null;
let bottom = null;
for (let i = 0, l = children.length; i < l; i++) {
let node = children[i];
let index = i;
if (hasConditionFlow(node)) {
node = node.conditionFlow;
index = node.index;
// skip flow items
i = index + (node as any).length - 1;
}
const rect = this.document.computeRect(node);
if (!rect) {
continue;
}
const distance = isPointInRect(e, rect) ? 0 : distanceToRect(e, rect);
if (distance === 0) {
nearDistance = distance;
nearNode = node;
nearIndex = index;
nearRect = rect;
break;
}
// TODO: 忘记为什么这么处理了,记得添加注释
if (top === null || rect.top < top) {
top = rect.top;
}
if (bottom === null || rect.bottom > bottom) {
bottom = rect.bottom;
}
if (nearDistance === null || distance < nearDistance) {
nearDistance = distance;
nearNode = node;
nearIndex = index;
nearRect = rect;
}
}
detail.index = nearIndex;
if (nearNode && nearRect) {
const el = getRectTarget(nearRect);
const inline = el ? isChildInline(el) : false;
const row = el ? isRowContainer(el.parentElement!) : false;
const vertical = inline || row;
// TODO: fix type
const near: any = {
node: nearNode,
pos: 'before',
align: vertical ? 'V' : 'H',
};
detail.near = near;
if (isNearAfter(e, nearRect, vertical)) {
near.pos = 'after';
detail.index = nearIndex + (isConditionFlow(nearNode) ? nearNode.length : 1);
}
if (!row && nearDistance !== 0) {
const edgeDistance = distanceToEdge(e, edge);
if (edgeDistance.distance < nearDistance!) {
const nearAfter = edgeDistance.nearAfter;
if (top == null) {
top = edge.top;
}
if (bottom == null) {
bottom = edge.bottom;
}
near.rect = new DOMRect(edge.left, top, edge.width, bottom - top);
near.align = 'H';
near.pos = nearAfter ? 'after' : 'before';
detail.index = nearAfter ? children.length : 0;
}
}
}
return this.document.createLocation(locationData);
*/
}
isAcceptable(container: NodeParent): boolean {
return false;
/*
const proto = container.prototype;
const view: any = this.getComponentInstance(container);
if (view && '$accept' in view) {
return true;
}
return proto.acceptable;*/
}
acceptAnyData(container: Node, e: LocateEvent | MouseEvent | KeyboardEvent) {
/*
const proto = container.prototype;
const view: any = this.document.getView(container);
// use view instance method: $accept
if (view && typeof view.$accept === 'function') {
// should return LocationData
return view.$accept(container, e);
}
// use prototype method: accept
return proto.accept(container, e);*/
}
checkNesting(dropTarget: Node, dragTarget: DragNodeObject | DragNodeDataObject): boolean {
return false;
/*
const items: Array<INode | NodeData> = dragTarget.nodes || (dragTarget as NodeDatasDragTarget).data;
return items.every(item => this.checkNestingDown(dropTarget, item));
*/
}
checkDropTarget(dropTarget: Node, dragTarget: DragNodeObject | DragNodeDataObject): boolean {
return false;
/*
const items: Array<INode | NodeData> = dragTarget.nodes || (dragTarget as NodeDatasDragTarget).data;
return items.every(item => this.checkNestingUp(dropTarget, item));
*/
}
checkNestingUp(parent: NodeParent, target: NodeData | Node): boolean {
/*
if (isElementNode(target) || isElementData(target)) {
const proto = isElementNode(target)
? target.prototype
: this.document.getPrototypeByTagNameOrURI(target.tagName, target.uri);
if (proto) {
return proto.checkNestingUp(target, parent);
}
}*/
return true;
}
checkNestingDown(parent: NodeParent, target: NodeData | Node): boolean {
/*
const proto = parent.prototype;
if (isConditionFlow(parent)) {
return parent.children.every(
child => proto.checkNestingDown(parent, child) && this.checkNestingUp(parent, child),
);
} else {
return proto.checkNestingDown(parent, target) && this.checkNestingUp(parent, target);
}*/
return false;
}
//#endregion
}

View File

@ -0,0 +1,2 @@
export * from './host';
export * from './host-view';

View File

@ -0,0 +1,80 @@
import { SimulatorRenderer } from '../renderer/renderer';
import { autorun, obx } from '@recore/obx';
import { SimulatorHost } from './host';
import { EventEmitter } from 'events';
const UNSET = Symbol('unset');
export type MasterProvider = (master: SimulatorHost) => any;
export type RendererConsumer<T> = (renderer: SimulatorRenderer, data: T) => Promise<any>;
// master 进程
// 0. 初始化该对象,因为需要响应变更发生在 master 进程
// 1. 提供消费数据或数据提供器,比如 Asset 资源,如果不是数据提供器,会持续提供
// 2. 收到成功通知
// renderer 进程
// 1. 持续消费,并持续监听数据
// 2. 消费
// 这里涉及俩个自定义项
// 1. 被消费数据协议
// 2. 消费机制(渲染进程自定 + 传递进入)
function isSimulatorRenderer(obj: any): obj is SimulatorRenderer {
return obj && obj.isSimulatorRenderer;
}
export default class ResourceConsumer<T = any> {
private emitter = new EventEmitter();
@obx.ref private _data: T | typeof UNSET = UNSET;
private _providing?: () => void;
constructor(provider: () => T, private consumer?: RendererConsumer<T>) {
this._providing = autorun(() => {
this._data = provider();
});
}
private _consuming?: () => void;
consume(consumerOrRenderer: SimulatorRenderer | ((data: T) => any)) {
if (this._consuming) {
return;
}
let consumer: (data: T) => any;
if (isSimulatorRenderer(consumerOrRenderer)) {
if (!this.consumer) {
// TODO: throw error
return;
}
const rendererConsumer = this.consumer!;
consumer = (data) => rendererConsumer(consumerOrRenderer, data);
} else {
consumer = consumerOrRenderer;
}
this._consuming = autorun(async () => {
if (this._data === UNSET) {
return;
}
await consumer(this._data);
// TODO: catch error and report
this.emitter.emit('consume');
});
}
dispose() {
if (this._providing) {
this._providing();
}
if (this._consuming) {
this._consuming();
}
this.emitter.removeAllListeners();
}
waitFirstConsume(): Promise<any> {
return new Promise((resolve) => {
this.emitter.once('consume', resolve);
});
}
}

View File

@ -0,0 +1,135 @@
import { obx, computed } from '@recore/obx';
import { Point } from '../../../designer/location';
import { ScrollTarget } from '../../../designer/scroller';
import { AutoFit, IViewport } from '../../../designer/simulator';
export default class Viewport implements IViewport {
@obx.ref private rect?: DOMRect;
private _bounds?: DOMRect;
get bounds(): DOMRect {
if (this._bounds) {
return this._bounds;
}
this._bounds = this.viewportElement!.getBoundingClientRect();
requestAnimationFrame(() => {
this._bounds = undefined;
});
return this._bounds;
}
get contentBounds(): DOMRect {
const bounds = this.bounds;
const scale = this.scale;
return new DOMRect(0, 0, bounds.width / scale, bounds.height / scale);
}
private viewportElement?: Element;
mount(viewportElement: Element | null) {
if (!viewportElement) {
return;
}
this.viewportElement = viewportElement;
this.touch();
}
touch() {
if (this.viewportElement) {
this.rect = this.bounds;
}
}
@computed get height(): number {
if (!this.rect) {
return 600;
}
return this.rect.height;
}
@computed get width(): number {
if (!this.rect) {
return 1000;
}
return this.rect.width;
}
/**
*
*/
get scale(): number {
if (!this.rect || this.contentWidth === AutoFit) {
return 1;
}
return this.width / this.contentWidth;
}
@obx.ref private _contentWidth: number | AutoFit = AutoFit;
get contentHeight(): number | AutoFit {
if (!this.rect || this.scale === 1) {
return AutoFit;
}
return this.height / this.scale;
}
get contentWidth(): number | AutoFit {
if (!this.rect || (this._contentWidth !== AutoFit && this._contentWidth <= this.width)) {
return AutoFit;
}
return this._contentWidth;
}
set contentWidth(val: number | AutoFit) {
this._contentWidth = val;
}
@obx.ref private _scrollX = 0;
@obx.ref private _scrollY = 0;
get scrollX() {
return this._scrollX;
}
get scrollY() {
return this._scrollY;
}
private _scrollTarget?: ScrollTarget;
/**
*
*/
get scrollTarget(): ScrollTarget | undefined {
return this._scrollTarget;
}
setScrollTarget(target: Window) {
const scrollTarget = new ScrollTarget(target);
this._scrollX = scrollTarget.left;
this._scrollY = scrollTarget.top;
target.onscroll = () => {
this._scrollX = scrollTarget.left;
this._scrollY = scrollTarget.top;
};
this._scrollTarget = scrollTarget;
}
toGlobalPoint(point: Point): Point {
if (!this.viewportElement) {
return point;
}
const rect = this.bounds;
return {
clientX: point.clientX * this.scale + rect.left,
clientY: point.clientY * this.scale + rect.top,
};
}
toLocalPoint(point: Point): Point {
if (!this.viewportElement) {
return point;
}
const rect = this.bounds;
return {
clientX: (point.clientX - rect.left) / this.scale,
clientY: (point.clientY - rect.top) / this.scale,
};
}
}

View File

@ -0,0 +1,5 @@
import { SimulatorHostView } from './host/host-view';
export * from './host/host';
export * from './host/host-view';
export default SimulatorHostView;

View File

@ -0,0 +1 @@
沙箱环境

View File

@ -0,0 +1,4 @@
// NOTE: 仅做类型标注,切勿做其它用途
import { SimulatorHost } from '../host';
export const host: SimulatorHost = (window as any).LCSimulatorHost;

View File

@ -0,0 +1,7 @@
import renderer from './renderer';
if (typeof window !== 'undefined') {
(window as any).SimulatorRenderer = renderer;
}
export default renderer;

View File

@ -0,0 +1,69 @@
// import { Engine as LowCodeRenderer } from '@ali/iceluna-sdk';
import { ReactInstance, Fragment, Component } from 'react';
import { observer } from '@recore/core-obx';
import { SimulatorRenderer } from './renderer';
import './renderer.less';
export default class SimulatorRendererView extends Component<{ renderer: SimulatorRenderer }> {
render() {
const { renderer } = this.props;
return (
<Layout renderer={renderer}>
<Renderer renderer={renderer} />
</Layout>
);
}
}
@observer
class Layout extends Component<{ renderer: SimulatorRenderer; }> {
shouldComponentUpdate() {
return false;
}
render() {
const { renderer, children } = this.props;
const layout = renderer.layout;
if (layout) {
const { Component, props } = layout;
return <Component props={props}>{children}</Component>;
}
return <Fragment>{children}</Fragment>;
}
}
@observer
class Renderer extends Component<{ renderer: SimulatorRenderer }> {
shouldComponentUpdate() {
return false;
}
render() {
const { renderer } = this.props;
return (
<LowCodeRenderer
schema={renderer.schema}
components={renderer.components}
appHelper={renderer.context}
// context={renderer.context}
designMode={renderer.designMode}
componentsMap={renderer.componentsMap}
suspended={renderer.suspended}
self={renderer.scope}
onComponentGetRef={(schema: any, ref: ReactInstance | null) => {
renderer.mountInstance(schema.id, ref);
}}
onComponentGetCtx={(schema: any, ctx: object) => {
renderer.mountContext(schema.id, ctx);
}}
/>
);
}
}
class LowCodeRenderer extends Component<any> {
render() {
return <div>{JSON.stringify(this.props.schema)}</div>
}
}

View File

@ -0,0 +1,10 @@
html {
padding-bottom: 30px;
background: transparent !important;
}
body, html {
display: block;
min-height: 100%;
background: white;
}

View File

@ -0,0 +1,259 @@
import { createElement, ReactInstance } from 'react';
import { render as reactRender } from 'react-dom';
import { host } from './host';
import SimulatorRendererView from './renderer-view';
import { computed, obx } from '@recore/obx';
import { RootSchema, NpmInfo } from '../../../designer/schema';
import { isElement } from '../../../utils/dom';
import { Asset } from '../utils/asset';
import loader from '../utils/loader';
import { ComponentDescriptionSpec } from '../../../designer/document/node/component-config';
let REACT_KEY = '';
function cacheReactKey(el: Element): Element {
if (REACT_KEY !== '') {
return el;
}
REACT_KEY = Object.keys(el).find(key => key.startsWith('__reactInternalInstance$')) || '';
if (!REACT_KEY && (el as HTMLElement).parentElement) {
return cacheReactKey((el as HTMLElement).parentElement!);
}
return el;
}
const SYMBOL_VNID = Symbol('_LCNodeId');
function getClosestNodeId(element: Element): string | null {
let el: any = element;
if (el) {
el = cacheReactKey(el);
}
while (el) {
if (SYMBOL_VNID in el) {
return el[SYMBOL_VNID];
}
if (el[REACT_KEY]) {
return getNodeId(el[REACT_KEY]);
}
el = el.parentElement;
}
return null;
}
function getNodeId(instance: any): string {
if (instance.stateNode && SYMBOL_VNID in instance.stateNode) {
return instance.stateNode[SYMBOL_VNID];
}
return getNodeId(instance.return);
}
function checkInstanceMounted(instance: any): boolean {
if (isElement(instance)) {
return instance.parentElement != null;
}
return true;
}
export class SimulatorRenderer {
readonly isSimulatorRenderer = true;
private dispose?: () => void;
constructor() {
if (!host) {
return;
}
this.dispose = host.connect(this, () => {
// sync layout config
// sync schema
this._schema = host.document.schema;
// sync designMode
// sync suspended
// sync scope
// sync device
});
host.componentsConsumer.consume(async (data) => {
if (data.componentsAsset) {
await this.load(data.componentsAsset);
}
// sync componetsMap
this._componentsMap = data.componentsMap;
});
host.injectionConsumer.consume((data) => {
// sync utils, i18n, contants,... config
this._appContext = {
utils: {},
constants: {
name: 'demo',
},
};
});
}
@computed get layout(): any {
// TODO: parse layout Component
return null;
}
@obx.ref private _schema?: RootSchema;
@computed get schema(): any {
return this._schema;
}
@obx.ref private _componentsMap = {};
@computed get components(): object {
// 根据 device 选择不同组件,进行响应式
// 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl
return buildComponents(this._componentsMap);
}
// context from: utils、constants、history、location、match
@obx.ref private _appContext = {};
@computed get context(): any {
return this._appContext;
}
@computed get designMode(): any {
return 'border';
}
@computed get componentsMap(): any {
return this._componentsMap;
}
@computed get suspended(): any {
return false;
}
@computed get scope(): any {
return null;
}
/**
*
*/
load(asset: Asset): Promise<any> {
return loader.load(asset);
}
private instancesMap = new Map<string, ReactInstance[]>();
mountInstance(id: string, instance: ReactInstance | null) {
const instancesMap = this.instancesMap;
if (instance == null) {
let instances = this.instancesMap.get(id);
if (instances) {
instances = instances.filter(checkInstanceMounted);
if (instances.length > 0) {
instancesMap.set(id, instances);
} else {
instancesMap.delete(id);
}
}
return;
}
if (isElement(instance)) {
cacheReactKey(instance);
} else if (!(instance as any)[SYMBOL_VNID]) {
const origUnmout = instance.componentWillUnmount;
// hack! delete instance from map
instance.componentWillUnmount = function() {
const instances = instancesMap.get(id);
if (instances) {
const i = instances.indexOf(instance);
if (i > -1) {
instances.splice(i, 1);
}
}
origUnmout && origUnmout.call(this);
};
}
(instance as any)[SYMBOL_VNID] = id;
let instances = this.instancesMap.get(id);
if (instances) {
instances = instances.filter(checkInstanceMounted);
instances.push(instance);
instancesMap.set(id, instances);
} else {
instancesMap.set(id, [instance]);
}
}
private ctxMap = new Map<string, object>();
mountContext(id: string, ctx: object) {
this.ctxMap.set(id, ctx);
}
getComponentInstance(id: string): ReactInstance[] | null {
return this.instancesMap.get(id) || null;
}
getClosestNodeId(element: Element): string | null {
return getClosestNodeId(element);
}
private _running: boolean = false;
run() {
if (this._running) {
return;
}
this._running = true;
const containerId = 'app';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
document.body.appendChild(container);
container.id = containerId;
}
reactRender(createElement(SimulatorRendererView, { renderer: this }), container);
}
}
function accessLibrary(library: string | object) {
if (typeof library !== 'string') {
return library;
}
return (window as any)[library];
}
function getSubComponent(component: any, paths: string[]) {
const l = paths.length;
if (l < 1) {
return component;
}
let i = 0;
while (i < l) {
const key = paths[i]!;
try {
component = (component as any)[key];
} catch (e) {
return null;
}
if (!component) {
return null;
}
i++;
}
return component;
}
function findComponent(componentName: string, npm?: NpmInfo) {
if (!npm) {
return accessLibrary(componentName);
}
const libraryName = npm.exportName || npm.componentName || componentName;
const component = accessLibrary(libraryName);
const paths = npm.subName ? npm.subName.split('.') : [];
if (npm.destructuring) {
paths.unshift(libraryName);
}
return getSubComponent(component, paths);
}
function buildComponents(componentsMap: { [componentName: string]: ComponentDescriptionSpec }) {
const components: any = {};
Object.keys(componentsMap).forEach(componentName => {
components[componentName] = findComponent(componentName, componentsMap[componentName].npm);
});
return components;
}
export default new SimulatorRenderer();

View File

@ -0,0 +1,76 @@
export interface AssetItem {
type: AssetType;
content?: string | null;
level?: AssetLevel;
id?: string;
}
export enum AssetLevel {
// 基础依赖库
BaseDepends = 1,
// 基础组件库
BaseComponents = 2,
// 主题包
Theme = 3,
// 运行时
Runtime = 4,
// 业务组件
Components = 5,
// 应用 & 页面
App = 6,
}
export type URL = string;
export enum AssetType {
JSUrl = 'jsUrl',
CSSUrl = 'cssUrl',
CSSText = 'cssText',
JSText = 'jsText',
Bundle = 'bundel',
}
export interface AssetBundle {
type: AssetType.Bundle;
level?: AssetLevel;
assets?: Asset | AssetList | null;
}
export type Asset = AssetList | AssetBundle | AssetItem | URL;
export type AssetList = Array<Asset | undefined | null>;
export function isAssetItem(obj: any): obj is AssetItem {
return obj && obj.type;
}
export function isAssetBundle(obj: any): obj is AssetBundle {
return obj && obj.type === AssetType.Bundle;
}
export function isCSSUrl(url: string): boolean {
return /\.css$/.test(url);
}
export function assetBundle(assets?: Asset | AssetList | null, level?: AssetLevel): AssetBundle | null {
if (!assets) {
return null;
}
return {
type: AssetType.Bundle,
assets,
level,
};
}
export function assetItem(type: AssetType, content?: string | null, level?: AssetLevel, id?: string): AssetItem | null {
if (content) {
return null;
}
return {
type,
content,
level,
id,
};
}

View File

@ -0,0 +1,284 @@
import { ReactElement, createElement, ReactType } from 'react';
import classNames from 'classnames';
const mocksCache = new Map<string, (props: any) => ReactElement>();
// endpoint element: input,select,video,audio,canvas,textarea
//
function getBlockElement(tag: string): (props: any) => ReactElement {
if (mocksCache.has(tag)) {
return mocksCache.get(tag)!;
}
const mock = ({ className, children, ...rest }: any = {}) => {
const props = {
...rest,
className: classNames('my-intrinsic-container', className),
};
return createElement(tag, props, children);
};
mock.prototypeConfig = {
uri: `@html:${tag}`,
selfControlled: true,
...(prototypeMap as any)[tag],
};
mocksCache.set(tag, mock);
return mock;
}
const HTMLBlock = [
'div',
'p',
'article',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'aside',
'blockquote',
'footer',
'form',
'header',
'table',
'tbody',
'section',
'ul',
'li',
'span',
];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const HTMLInlineBlock = ['a', 'b', 'span', 'em'];
export function getIntrinsicMock(tag: string): ReactType {
if (HTMLBlock.indexOf(tag) > -1) {
return getBlockElement(tag);
}
return tag as any;
}
const prototypeMap = {
div: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: 'p',
},
},
ul: {
isContainer: true,
selfControlled: true,
nesting: {
childWhitelist: 'li',
},
},
p: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: 'button,p',
},
},
li: {
isContainer: true,
selfControlled: true,
nesting: {
parentWhitelist: 'ui,ol',
},
},
span: {
isContainer: true,
selfControlled: true,
},
a: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!a',
},
},
b: {
isContainer: true,
selfControlled: true,
},
strong: {
isContainer: true,
selfControlled: true,
},
em: {
isContainer: true,
selfControlled: true,
},
i: {
isContainer: true,
selfControlled: true,
},
form: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!form,!button',
},
},
table: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
caption: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
select: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
button: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
input: {
isContainer: false,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button,!h1,!h2,!h3,!h4,!h5',
},
},
textarea: {
isContainer: false,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
image: {
isContainer: false,
selfControlled: true,
},
canvas: {
isContainer: false,
selfControlled: true,
},
br: {
isContainer: false,
selfControlled: true,
},
h1: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
h2: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
h3: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
h4: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
h5: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
h6: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
article: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
aside: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
footer: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
header: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
blockquote: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
address: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
section: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!p,!h1,!h2,!h3,!h4,!h5,!h6,!button',
},
},
summary: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
nav: {
isContainer: true,
selfControlled: true,
nesting: {
ancestorBlacklist: '!button',
},
},
};

View File

@ -0,0 +1,105 @@
import { load, evaluate } from './script';
import StylePoint from './style';
import { Asset, AssetLevel, AssetType, AssetList, isAssetBundle, isAssetItem, assetItem, isCSSUrl, AssetItem } from './asset';
function parseAssetList(scripts: any, styles: any, assets: AssetList, level?: AssetLevel) {
for (let asset of assets) {
parseAsset(scripts, styles, asset, level);
}
}
function parseAsset(scripts: any, styles: any, asset: Asset | undefined | null, level?: AssetLevel) {
if (!asset) {
return;
}
if (Array.isArray(asset)) {
return parseAssetList(scripts, styles, asset, level);
}
if (isAssetBundle(asset)) {
if (asset.assets) {
if (Array.isArray(asset.assets)) {
parseAssetList(scripts, styles, asset.assets, asset.level || level);
} else {
parseAsset(scripts, styles, asset.assets, asset.level || level);
}
return;
}
return;
}
if (!isAssetItem(asset)) {
asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!;
}
let lv = asset.level || level;
if (!lv || AssetLevel[lv] == null) {
lv = AssetLevel.App
}
asset.level = lv;
if (asset.type === AssetType.CSSUrl || asset.type == AssetType.CSSText) {
styles[lv].push(asset);
} else {
scripts[lv].push(asset);
}
}
export class AssetLoader {
async load(asset: Asset) {
const styles: any = {};
const scripts: any = {};
Object.keys(AssetLevel).forEach((key) => {
const v = (AssetLevel as any)[key];
styles[v] = [];
scripts[v] = [];
});
parseAsset(scripts, styles, asset);
const styleQueue: AssetItem[] = styles[AssetLevel.BaseDepends].concat(
styles[AssetLevel.BaseComponents],
styles[AssetLevel.Theme],
styles[AssetLevel.Runtime],
styles[AssetLevel.App],
);
const scriptQueue: AssetItem[] = scripts[AssetLevel.BaseDepends].concat(
scripts[AssetLevel.BaseComponents],
scripts[AssetLevel.Theme],
scripts[AssetLevel.Runtime],
scripts[AssetLevel.App],
);
await Promise.all(
styleQueue.map(({ content, level, type, id }) => this.loadStyle(content, level!, type === AssetType.CSSUrl, id)),
);
await Promise.all(
scriptQueue.map(({ content, type }) => this.loadScript(content, type === AssetType.JSUrl)),
);
}
private stylePoints = new Map<string, StylePoint>();
private loadStyle(content: string | undefined | null, level: AssetLevel, isUrl?: boolean, id?: string) {
if (!content) {
return;
}
let point: StylePoint | undefined;
if (id) {
point = this.stylePoints.get(id);
if (!point) {
point = new StylePoint(level, id);
this.stylePoints.set(id, point);
}
} else {
point = new StylePoint(level);
}
return isUrl ? point.applyUrl(content) : point.applyText(content);
}
private loadScript(content: string | undefined | null, isUrl?: boolean) {
if (!content) {
return;
}
return isUrl ? load(content) : evaluate(content);
}
}
export default new AssetLoader();

View File

@ -1,5 +1,5 @@
import { ReactInstance } from 'react';
import { isDOMNode, isElement } from './dom';
import { isDOMNode, isElement } from '../../../utils/dom';
const FIBER_KEY = '_reactInternalFiber';

View File

@ -1,4 +1,4 @@
import { createDefer } from './create-defer';
import { createDefer } from '../../../utils/create-defer';
export function evaluate(script: string) {
const scriptEl = document.createElement('script');
@ -36,3 +36,19 @@ export function load(url: string) {
return i.promise();
}
export function evaluateExpression(expr: string) {
// eslint-disable-next-line no-new-func
const fn = new Function(expr);
return fn();
}
export function newFunction(args: string, code: string) {
try {
// eslint-disable-next-line no-new-func
return new Function(args, code);
} catch (e) {
console.warn('Caught error, Cant init func');
return null;
}
}

View File

@ -0,0 +1,73 @@
import { createDefer } from '../../../utils/create-defer';
export default class StylePoint {
private lastContent: string | undefined;
private lastUrl: string | undefined;
private placeholder: Element | Text;
constructor(readonly level: number, readonly id?: string) {
let placeholder: any;
if (id) {
placeholder = document.head.querySelector(`style[data-id="${id}"]`);
}
if (!placeholder) {
placeholder = document.createTextNode('');
const meta = document.head.querySelector(`meta[level="${level}"]`);
if (meta) {
document.head.insertBefore(placeholder, meta);
} else {
document.head.appendChild(placeholder);
}
}
this.placeholder = placeholder;
}
applyText(content: string) {
if (this.lastContent === content) {
return;
}
this.lastContent = content;
this.lastUrl = undefined;
const element = document.createElement('style');
element.setAttribute('type', 'text/css');
if (this.id) {
element.setAttribute('data-id', this.id);
}
element.appendChild(document.createTextNode(content));
document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null);
document.head.removeChild(this.placeholder);
this.placeholder = element;
}
applyUrl(url: string) {
if (this.lastUrl === url) {
return;
}
this.lastContent = undefined;
this.lastUrl = url;
const element = document.createElement('link');
element.onload = onload;
element.onerror = onload;
const i = createDefer();
function onload(e: any) {
element.onload = null;
element.onerror = null;
if (e.type === 'load') {
i.resolve();
} else {
i.reject();
}
}
element.href = url;
element.rel = 'stylesheet';
if (this.id) {
element.setAttribute('data-id', this.id);
}
document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null);
document.head.removeChild(this.placeholder);
this.placeholder = element;
return i.promise();
}
}

View File

@ -0,0 +1,198 @@
import PropTypes from 'prop-types';
export const primitiveTypeMaps = {
string: {
defaultValue: '',
display: 'inline',
setter: 'TextSetter',
},
number: {
display: 'inline',
setter: 'NumberSetter' // extends TextSetter
},
array: {
defaultValue: [],
display: 'inline',
// itemType: any
setter: 'ArraySetter' // extends ExpressionSetter
},
bool: {
defaultValue: false,
display: 'inline',
setter: 'BoolSetter'
},
func: {
defaultValue: () => {},
display: 'inline',
setter: 'FunctionSetter' // extends ExpressionSetter
},
object: {
defaultValue: {},
display: 'inline',
// itemType: any
setter: 'ObjectSetter' // extends ExpressionSetter
},
// Anything that can be rendered: numbers, strings, elements or an array
// (or fragment) containing these types.
node: {
defaultValue: '',
display: 'inline',
setter: 'FragmentSetter',
},
// A React element.
element: {
display: 'inline',
setter: 'JSXSetter', // extends ExpressionSetter
},
symbol: {
display: 'inline',
setter: 'ExpressionSetter',
},
any: {
display: 'inline',
setter: 'ExpressionSetter',
}
};
function makeRequired(propType, visionType) {
function visionCheckTypeIsRequired(...rest) {
return propType.isRequired(...rest);
}
visionCheckTypeIsRequired.visionType = {
...visionType,
required: true,
};
return visionCheckTypeIsRequired;
}
function define(propType = PropTypes.any, visionType = {}) {
if (!propType._inner && propType.name !== 'visionCheckType') {
propType.visionType = visionType;
}
function visionCheckType(...rest) {
return propType(...rest);
}
visionCheckType.visionType = visionType;
visionCheckType.isRequired = makeRequired(propType, visionType);
return visionCheckType;
}
const VisionTypes = {
...PropTypes,
define,
};
export default VisionTypes;
// override primitive type chechers
Object.keys(primitiveTypeMaps).forEach((type) => {
const propType = PropTypes[type];
if (!propType) {
return;
}
propType._inner = true;
VisionTypes[type] = define(propType, primitiveTypeMaps[type]);
});
// You can ensure that your prop is limited to specific values by treating
// it as an enum.
VisionTypes.oneOf = (list) => {
return define(PropTypes.oneOf(list), {
defaultValue: list && list[0],
display: 'inline',
setter: {
type: 'SelectSetter',
options: list,
},
});
};
// An array of a certain type
VisionTypes.arrayOf = (type) => {
return define(PropTypes.arrayOf(type), {
defaultValue: [],
display: 'inline',
setter: {
type: 'ArraySetter', // list
itemType: type.visionType || primitiveTypeMaps.any, // addable type
}
});
};
// An object with property values of a certain type
VisionTypes.objectOf = (type) => {
return define(PropTypes.objectOf(type), {
defaultValue: {},
display: 'inline',
setter: {
type: 'ObjectSetter', // all itemType
itemType: type.visionType || primitiveTypeMaps.any, // addable type
}
});
};
// An object that could be one of many types
VisionTypes.oneOfType = (types) => {
const itemType = types.map(type => type.visionType || primitiveTypeMaps.any);
return define(PropTypes.oneOfType(types), {
defaultValue: itemType[0] && itemType[0].defaultValue,
display: 'inline',
setter: {
type: 'OneOfTypeSetter',
itemType, // addable type
},
});
};
// You can also declare that a prop is an instance of a class. This uses
// JS's instanceof operator.
VisionTypes.instanceOf = (classType) => {
return define(PropTypes.instanceOf(classType), {
display: 'inline',
setter: 'ExpressionSetter',
});
};
// An object with warnings on extra properties
VisionTypes.exact = (typesMap) => {
const exactTypes = {};
const defaultValue = {};
Object.keys(typesMap).forEach(key => {
exactTypes[key] = typesMap[key].visionType || primitiveTypeMaps.any;
defaultValue[key] = exactTypes[key].defaultValue;
});
return define(PropTypes.exact(typesMap), {
defaultValue,
display: 'inline',
setter: {
type: 'ObjectSetter', // all itemType
exactTypes,
},
});
}
// An object taking on a particular shape
VisionTypes.shape = (typesMap) => {
const exactTypes = {};
const defaultValue = {};
Object.keys(typesMap).forEach(key => {
exactTypes[key] = typesMap[key].visionType || primitiveTypeMaps.any;
defaultValue[key] = exactTypes[key].defaultValue;
});
return define(PropTypes.shape(typesMap), {
defaultValue,
display: 'inline',
setter: {
type: 'ObjectSetter', // all itemType
exactTypes,
itemType: primitiveTypeMaps.any, // addable type
},
});
};
// color
// time
// date
// range

View File

@ -0,0 +1,23 @@
import { EventEmitter } from 'events';
import { LocationDetail } from './location';
import Node, { isNode } from './document/node/node';
interface ActiveTarget {
node: Node;
detail?: LocationDetail;
}
export default class ActiveTracker {
private emitter = new EventEmitter();
track(target: ActiveTarget | Node) {
this.emitter.emit('change', isNode(target) ? { node: target } : target);
}
onChange(fn: (target: ActiveTarget) => void): () => void {
this.emitter.addListener('change', fn);
return () => {
this.emitter.removeListener('change', fn);
};
}
}

View File

@ -0,0 +1,51 @@
import { Component } from 'react';
import classNames from 'classnames';
import Designer, { DesignerProps } from './designer';
import BuiltinDragGhostComponent from '../builtins/drag-ghost';
import ProjectView from './project-view';
import './designer.less';
export default class DesignerView extends Component<DesignerProps> {
readonly designer: Designer;
constructor(props: any) {
super(props);
this.designer = new Designer(props);
}
shouldComponentUpdate(nextProps: DesignerProps) {
this.designer.setProps(nextProps);
const props = this.props;
if (
nextProps.className !== props.className ||
nextProps.style != props.style ||
nextProps.dragGhostComponent !== props.dragGhostComponent
) {
return true;
}
return false;
}
componentDidMount() {
const { onMount } = this.props;
if (onMount) {
onMount(this.designer);
}
}
componentWillMount() {
this.designer.purge();
}
render() {
const { className, style, dragGhostComponent } = this.props;
const DragGhost = dragGhostComponent || BuiltinDragGhostComponent;
return (
<div className={classNames('lc-designer', className)} style={style}>
<DragGhost designer={this.designer} />
<ProjectView designer={this.designer} />
</div>
);
}
}

View File

@ -1,6 +1,6 @@
import { ComponentType } from 'react';
import { obx, computed } from '@recore/obx';
import { SimulatorView as BuiltinSimulatorView } from '../builtins/simulator';
import BuiltinSimulatorView from '../builtins/simulator';
import Project from './project';
import { ProjectSchema } from './schema';
import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './dragon';

View File

@ -0,0 +1,349 @@
import Project from '../project';
import { RootSchema, NodeData, isDOMText, isJSExpression, NodeSchema } from '../schema';
import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './node/node';
import { Selection } from './selection';
import RootNode from './node/root-node';
import { ISimulator, ComponentInstance, Component } from '../simulator';
import { computed, obx } from '@recore/obx';
import Location from '../location';
import { ComponentConfig } from './node/component-config';
import { isElement } from '../../utils/dom';
export default class DocumentModel {
/**
* Page/Component/Block
*/
readonly rootNode: RootNode;
/**
*
*/
readonly id: string;
/**
*
*/
readonly selection: Selection = new Selection(this);
/**
*
*/
// TODO
// readonly history: History = new History(this);
private nodesMap = new Map<string, Node>();
private nodes = new Set<Node>();
private seqId = 0;
private _simulator?: ISimulator;
/**
*
*/
get simulator(): ISimulator | null {
return this._simulator || null;
}
get fileName(): string {
return (this.rootNode.extras.get('fileName')?.value as string) || this.id;
}
set fileName(fileName: string) {
this.rootNode.extras.get('fileName', true).value = fileName;
}
constructor(readonly project: Project, schema: RootSchema) {
this.rootNode = new RootNode(this, schema);
this.id = this.rootNode.id;
}
readonly designer = this.project.designer;
/**
* id
*/
nextId() {
return (++this.seqId).toString(36).toLocaleLowerCase();
}
/**
* id
*/
getNode(id: string): Node | null {
return this.nodesMap.get(id) || null;
}
/**
*
*/
hasNode(id: string): boolean {
const node = this.getNode(id);
return node ? !node.isPurged : false;
}
/**
* schema
*/
createNode(data: NodeData): Node {
let schema: any;
if (isDOMText(data) || isJSExpression(data)) {
schema = {
componentName: '#frag',
children: data,
};
} else {
schema = data;
}
const node = new Node(this, schema);
this.nodesMap.set(node.id, node);
this.nodes.add(node);
return node;
}
/**
*
*/
insertNode(parent: NodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node {
return insertChild(parent, thing, at, copy);
}
/**
*
*/
insertNodes(parent: NodeParent, thing: Node[] | NodeData[], at?: number | null, copy?: boolean) {
return insertChildren(parent, thing, at, copy);
}
/**
*
*/
removeNode(idOrNode: string | Node) {
let id: string;
let node: Node | null;
if (typeof idOrNode === 'string') {
id = idOrNode;
node = this.getNode(id);
} else {
node = idOrNode;
id = node.id;
}
if (!node) {
return;
}
this.internalRemoveAndPurgeNode(node);
}
/**
*
*/
internalRemoveAndPurgeNode(node: Node) {
if (!this.nodes.has(node)) {
return;
}
this.nodesMap.delete(node.id);
this.nodes.delete(node);
node.remove();
}
@obx.ref private _dropLocation: Location | null = null;
/**
*
*/
internalSetDropLocation(loc: Location | null) {
this._dropLocation = loc;
}
/**
*
*/
get dropLocation() {
return this._dropLocation;
}
/**
*
*/
wrapWith(schema: NodeSchema): Node | null {
const nodes = this.selection.getTopNodes();
if (nodes.length < 1) {
return null;
}
const wrapper = this.createNode(schema);
if (isNodeParent(wrapper)) {
const first = nodes[0];
// TODO: check nesting rules x 2
insertChild(first.parent!, wrapper, first.index);
insertChildren(wrapper, nodes);
this.selection.select(wrapper.id);
return wrapper;
}
this.removeNode(wrapper);
return null;
}
/**
* schema
*/
get schema(): RootSchema {
return this.rootNode.schema as any;
}
/**
*
*/
getNodeSchema(id: string): NodeData | null {
const node = this.getNode(id);
if (node) {
return node.schema;
}
return null;
}
/**
*
*/
isModified() {
// return !this.history.isSavePoint();
}
/**
*
*/
@computed get simulatorProps(): object {
let simulatorProps = this.designer.simulatorProps;
if (typeof simulatorProps === 'function') {
simulatorProps = simulatorProps(this);
}
return {
...simulatorProps,
documentContext: this,
onMount: this.mountSimulator.bind(this),
};
}
private mountSimulator(simulator: ISimulator) {
this._simulator = simulator;
// TODO: emit simulator mounted
}
/**
* simulator
*/
getComponentInstance(node: Node): ComponentInstance[] | null {
if (this.simulator) {
this.simulator.getComponentInstance(node);
}
return null;
}
getComponent(componentName: string): any {
return this.simulator!.getComponent(componentName);
}
getComponentConfig(component: Component, componentName: string): ComponentConfig {
// TODO: guess componentConfig from component by simulator
return this.designer.getComponentConfig(componentName);
}
/**
* DOM simulator
*/
getNodeFromElement(target: Element | null): Node | null {
if (!this.simulator || !target) {
return null;
}
const id = this.simulator.getClosestNodeId(target);
if (!id) {
return null;
}
return this.getNode(id) as Node;
}
/**
*
* DOM simulator
*/
getDOMNodes(instance: ComponentInstance): Array<Element | Text> | null {
if (!this.simulator) {
return null;
}
if (isElement(instance)) {
return [instance];
}
return this.simulator.findDOMNodes(instance);
}
private _opened: boolean = true;
private _suspensed: boolean = false;
/**
* 
*/
get suspensed(): boolean {
return this._suspensed || !this._opened;
}
/**
* suspensed 
*/
get actived(): boolean {
return !this._suspensed;
}
/**
*
*/
get opened() {
return this._opened;
}
/**
*
* tab
*/
private setSuspense(flag: boolean) {
if (!this._opened && !flag) {
return;
}
this._suspensed = flag;
this.simulator?.setSuspense(flag);
if (!flag) {
this.project.checkExclusive(this);
}
}
suspense() {
this.setSuspense(true);
}
active() {
this.setSuspense(false);
}
/**
*
*/
open(): void {
this._opened = true;
if (this._suspensed) {
this.setSuspense(false);
}
}
/**
* sleep
*/
close(): void {
this.setSuspense(true);
this._opened = false;
}
/**
*
*/
remove() {}
}
export function isDocumentModel(obj: any): obj is DocumentModel {
return obj && obj.rootNode;
}

View File

@ -0,0 +1,35 @@
import { Component } from 'react';
import DocumentModel from './document-model';
import { observer } from '@recore/core-obx';
import classNames from 'classnames';
@observer
export default class DocumentView extends Component<{ document: DocumentModel }> {
shouldComponentUpdate() {
return false;
}
render() {
const { document } = this.props;
const simulatorProps = document.simulatorProps;
const Simulator = document.designer.simulatorComponent;
return (
<div
className={classNames('lc-document', {
'lc-document-hidden': document.suspensed,
})}
>
{/* 这一层将来做缩放用途 */}
<div className="lc-simulator-shell">
<Simulator {...simulatorProps} />
</div>
<DocumentInfoView document={document} />
</div>
);
}
}
class DocumentInfoView extends Component<{ document: DocumentModel }> {
render() {
return null;
}
}

View File

@ -0,0 +1,380 @@
import { ReactNode, ReactElement, ComponentType } from 'react';
import Node, { NodeParent } from './node';
import { NodeData, NodeSchema } from '../../schema';
export type BasicTypes = 'array' | 'bool' | 'func' | 'number' | 'object' | 'string' | 'node' | 'element' | 'any';
export interface CompositeType {
type: BasicTypes;
isRequired: boolean;
}
// TODO: add complex types
export interface PropConfig {
name: string;
propType: BasicTypes | CompositeType;
description?: string;
defaultValue?: any;
}
export type CustomView = ReactElement | ComponentType<any>;
export interface TipConfig {
className?: string;
children?: ReactNode;
theme?: string;
direction?: string; // 'n|s|w|e|top|bottom|left|right';
}
export interface IconConfig {
name: string;
size?: string;
className?: string;
effect?: string;
}
export interface TitleConfig {
label?: ReactNode;
tip?: string | ReactElement | TipConfig;
icon?: string | ReactElement | IconConfig;
className?: string;
}
export type Title = string | ReactElement | TitleConfig;
export enum DisplayType {
Inline = 'inline',
Block = 'block',
Accordion = 'Accordion',
Plain = 'plain',
Caption = 'caption',
}
export interface SetterConfig {
/**
* if *string* passed must be a registered Setter Name
*/
componentName: string | CustomView;
/**
* the props pass to Setter Component
*/
props?: {
[prop: string]: any;
};
}
/**
* if *string* passed must be a registered Setter Name
*/
export type SetterType = SetterConfig | string | CustomView;
export interface SettingFieldConfig {
/**
* the name of this setting field, which used in quickEditor
*/
name: string;
/**
* the field body contains
*/
setter: SetterType;
/**
* the prop target which to set, eg. "style.width"
* @default sameas .name
*/
propTarget?: string;
/**
* the field title
* @default sameas .propTarget
*/
title?: Title;
extraProps?: {
/**
* default value of target prop for setter use
*/
defaultValue?: any;
onChange?: (value: any) => void;
getValue?: () => any;
/**
* the field conditional show, is not set always true
* @default undefined
*/
condition?: (node: Node) => boolean;
/**
* quick add "required" validation
*/
required?: boolean;
/**
* the field display
* @default DisplayType.Block
*/
display?: DisplayType.Inline | DisplayType.Block | DisplayType.Accordion | DisplayType.Plain;
/**
* default collapsed when display accordion
*/
defaultCollapsed?: boolean;
/**
* layout control
* number or [column number, left offset]
* @default 6
*/
span?: number | [number, number];
};
}
export interface SettingGroupConfig {
/**
* the type "group"
*/
type: 'group';
/**
* the name of this setting group, which used in quickEditor
*/
name?: string;
/**
* the setting items which group body contains
*/
items: Array<SettingFieldConfig | SettingGroupConfig | CustomView>;
/**
* the group title
* @default sameas .name
*/
title?: Title;
extraProps: {
/**
* the field conditional show, is not set always true
* @default undefined
*/
condition?: (node: Node) => boolean;
/**
* the group display
* @default DisplayType.Block
*/
display?: DisplayType.Block | DisplayType.Accordion;
/**
* default collapsed when display accordion
*/
defaultCollapsed?: boolean;
/**
* the gap between span
* @default 0 px
*/
gap?: number;
/**
* layout control
* number or [column number, left offset]
* @default 6
*/
span?: number | [number, number];
};
}
export type PropSettingConfig = SettingFieldConfig | SettingGroupConfig | CustomView;
export interface NestingRule {
childWhitelist?: string[];
parentWhitelist?: string[];
}
export interface Configure {
props?: PropSettingConfig[];
styles?: object;
events?: object;
component?: {
isContainer?: boolean;
isModal?: boolean;
descriptor?: string;
nestingRule?: NestingRule;
};
}
export interface ComponentDescriptionSpec {
componentName: string;
/**
* unique id
*/
uri?: string;
/**
* title or description
*/
title?: string;
/**
* svg icon for component
*/
icon?: string | ReactNode;
tags?: string[];
description?: string;
docUrl?: string;
screenshot?: string;
devMode?: 'procode' | 'lowcode';
npm?: {
package: string;
exportName: string;
subName: string;
main: string;
destructuring: boolean;
version: string;
};
props?: PropConfig[];
configure?: PropSettingConfig[] | Configure;
}
function ensureAList(list?: string | string[]): string[] | null {
if (!list) {
return null;
}
if (!Array.isArray(list)) {
list = list.split(/ *[ ,|] */).filter(Boolean);
}
if (list.length < 1) {
return null;
}
return list;
}
function npmToURI(npm: {
package: string;
exportName?: string;
subName?: string;
destructuring?: boolean;
main?: string;
version: string;
}): string {
let pkg = [];
if (npm.package) {
pkg.push(npm.package);
}
if (npm.main) {
if (npm.main[0] === '/') {
pkg.push(npm.main.slice(1));
} else if (npm.main.slice(0, 2) === './') {
pkg.push(npm.main.slice(2));
} else {
pkg.push(npm.main);
}
}
let uri = pkg.join('/');
uri += `:${npm.destructuring && npm.exportName ? npm.exportName : 'default'}`;
if (npm.subName) {
uri += `.${npm.subName}`;
}
return uri;
}
function generatePropsConfigure(props: PropConfig[]) {
// todo:
return [];
}
export class ComponentConfig {
readonly isComponentConfig = true;
private _uri?: string;
get uri(): string {
return this._uri!;
}
private _componentName?: string;
get componentName(): string {
return this._componentName!;
}
private _isContainer?: boolean;
get isContainer(): boolean {
return this._isContainer!;
}
private _isModal?: boolean;
get isModal(): boolean {
return this._isModal!;
}
private _descriptor?: string;
get descriptor(): string {
return this._descriptor!;
}
private _acceptable?: boolean;
get acceptable(): boolean {
return this._acceptable!;
}
private _configure?: Configure;
get configure(): Configure {
return this._configure!;
}
private parentWhitelist?: string[] | null;
private childWhitelist?: string[] | null;
get title() {
return this._spec.title;
}
get icon() {
return this._spec.icon;
}
get propsConfigure() {
return this.configure.props;
}
constructor(private _spec: ComponentDescriptionSpec) {
this.parseSpec(_spec);
}
private parseSpec(spec: ComponentDescriptionSpec) {
const { componentName, uri, configure, npm, props } = spec;
this._uri = uri || (npm ? npmToURI(npm) : componentName);
this._componentName = componentName;
this._acceptable = false;
if (!configure || Array.isArray(configure)) {
this._configure = {
props: !configure ? [] : configure,
styles: {
supportClassName: true,
supportInlineStyle: true,
},
};
} else {
this._configure = configure;
}
if (!this.configure.props) {
this.configure.props = props ? generatePropsConfigure(props) : [];
}
const { component } = this.configure;
if (component) {
this._isContainer = component.isContainer ? true : false;
this._isModal = component.isModal ? true : false;
this._descriptor = component.descriptor;
if (component.nestingRule) {
const { parentWhitelist, childWhitelist } = component.nestingRule;
this.parentWhitelist = ensureAList(parentWhitelist);
this.childWhitelist = ensureAList(childWhitelist);
}
} else {
this._isContainer = false;
this._isModal = false;
}
}
set spec(spec: ComponentDescriptionSpec) {
this._spec = spec;
this.parseSpec(spec);
}
get spec(): ComponentDescriptionSpec {
return this._spec;
}
checkNestingUp(my: Node | NodeData, parent: NodeParent) {
if (this.parentWhitelist) {
return this.parentWhitelist.includes(parent.componentName);
}
return true;
}
checkNestingDown(my: Node, target: Node | NodeSchema) {
if (this.childWhitelist) {
return this.childWhitelist.includes(target.componentName);
}
return true;
}
}

View File

@ -0,0 +1,186 @@
import Node, { NodeParent } from './node';
import { NodeData } from '../../schema';
export default class NodeChildren {
private children: Node[];
constructor(readonly owner: NodeParent, childrenData: NodeData | NodeData[]) {
this.children = (Array.isArray(childrenData) ? childrenData : [childrenData]).map(child => {
const node = this.owner.document.createNode(child);
node.internalSetParent(this.owner);
return node;
});
}
/**
* schema
* @param serialize id
*/
exportSchema(serialize = false): NodeData[] {
return this.children.map(node => node.exportSchema(serialize));
}
/**
*
*/
get size(): number {
return this.children.length;
}
/**
*
*/
isEmpty() {
return this.size < 1;
}
/*
// 用于数据重新灌入
merge() {
for (let i = 0, l = data.length; i < l; i++) {
const item = this.children[i];
if (item && isMergeable(item) && item.tagName === data[i].tagName) {
item.merge(data[i]);
} else {
if (item) {
item.purge();
}
this.children[i] = this.document.createNode(data[i]);
this.children[i].internalSetParent(this);
}
}
if (this.children.length > data.length) {
this.children.splice(data.length).forEach(child => child.purge());
}
}
*/
/**
*
*/
delete(node: Node, purge: boolean = false): boolean {
const i = this.children.indexOf(node);
if (i < 0) {
return false;
}
const deleted = this.children.splice(i, 1)[0];
if (purge) {
// should set parent null
deleted.internalSetParent(null);
deleted.purge();
}
return false;
}
/**
*
*/
insert(node: Node, at?: number | null): void {
const children = this.children;
let index = at == null || at === -1 ? children.length : at;
const i = children.indexOf(node);
if (i < 0) {
if (index < children.length) {
children.splice(index, 0, node);
} else {
children.push(node);
}
node.internalSetParent(this.owner);
} else {
if (index > i) {
index -= 1;
}
if (index === i) {
return;
}
children.splice(i, 1);
children.splice(index, 0, node);
}
// check condition group
node.conditionGroup = null;
if (node.prevSibling && node.nextSibling) {
const conditionGroup = node.prevSibling.conditionGroup;
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
node.conditionGroup = conditionGroup;
}
}
}
/**
*
*/
indexOf(node: Node): number {
return this.children.indexOf(node);
}
/**
*
*/
get(index: number): Node | null {
return this.children[index] || null;
}
/**
*
*/
has(node: Node) {
return this.indexOf(node) > -1;
}
/**
*
*/
[Symbol.iterator](): { next(): { value: Node } } {
let index = 0;
const children = this.children;
const length = children.length || 0;
return {
next() {
if (index < length) {
return {
value: children[index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
*
*/
forEach(fn: (item: Node, index: number) => void): void {
this.children.forEach((child, index) => {
return fn(child, index);
});
}
/**
*
*/
map<T>(fn: (item: Node, index: number) => T): T[] | null {
return this.children.map((child, index) => {
return fn(child, index);
});
}
private purged = false;
/**
*
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.children.forEach(child => child.purge());
}
}

View File

@ -0,0 +1,86 @@
import { obx } from '@recore/obx';
import { JSExpression, isJSExpression } from '../../schema';
export default class NodeContent {
@obx.ref private _value: string | JSExpression = '';
get value(): string | JSExpression {
return this._value;
}
set value(val: string | JSExpression) {
this._value = val;
}
/**
*
*/
get code() {
if (isJSExpression(this._value)) {
return this._value.value;
}
return JSON.stringify(this.value);
}
/**
*
*/
set code(code: string) {
if (isJSExpression(this._value)) {
this._value = {
...this._value,
value: code,
};
} else {
let useCode: boolean = true;
try {
const v = JSON.parse(code);
const t = typeof v;
if (v == null) {
this._value = '';
useCode = false;
} else if (t === 'string' || t === 'number' || t === 'boolean') {
this._value = String(v);
useCode = false;
}
} catch (e) {
// ignore
}
if (useCode) {
this._value = {
type: 'JSExpression',
value: code,
mock: this._value,
};
}
}
}
constructor(value: any) {
const type = typeof value;
if (value == null) {
this._value = '';
} else if (type === 'string' || type === 'number' || type === 'boolean') {
this._value = String(value);
} else if (isJSExpression(value)) {
this._value = value;
}
}
/**
*
*/
isJSExpression(): boolean {
return isJSExpression(this._value);
}
/**
*
*/
isEmpty() {
if (isJSExpression(this._value)) {
return this._value.value === '';
}
return this._value === '';
}
}

View File

@ -0,0 +1,481 @@
import { obx } from '@recore/obx';
import { NodeSchema, NodeData, PropsMap, PropsList } from '../../schema';
import Props from './props/props';
import DocumentModel from '../document-model';
import NodeChildren from './node-children';
import Prop from './props/prop';
import NodeContent from './node-content';
import { Component } from '../../simulator';
import { ComponentConfig } from './component-config';
const DIRECTIVES = ['condition', 'conditionGroup', 'loop', 'loopArgs', 'title', 'ignore', 'hidden', 'locked'];
/**
*
*
* [Node Properties]
* componentName: Page/Block/Component
* props
* children
*
* [Directives]
* loop
* loopArgs
* condition
* ------- future support -----
* conditionGroup
* title
* ignore
* locked
* hidden
*/
export default class Node {
/**
*
*/
readonly isNode = true;
/**
* id
*/
readonly id: string;
/**
*
* :
* * #text
* * #expression
* * Page
* * Block/Fragment
* * Component /
*/
readonly componentName: string;
protected _props?: Props<Node>;
protected _directives?: Props<Node>;
protected _extras?: Props<Node>;
protected _children: NodeChildren | NodeContent;
private _parent: NodeParent | null = null;
private _zLevel = 0;
get props(): Props<Node> | undefined {
return this._props;
}
get directives(): Props<Node> | undefined {
return this._directives;
}
get extras(): Props<Node> | undefined {
return this._extras;
}
/**
*
*/
get parent(): NodeParent | null {
return this._parent;
}
/**
*
*/
get children(): NodeChildren | NodeContent {
return this._children;
}
/**
*
*/
get zLevel(): number {
return this._zLevel;
}
constructor(readonly document: DocumentModel, nodeSchema: NodeSchema) {
const { componentName, id, children, props, ...extras } = nodeSchema;
this.id = id || `node$${document.nextId()}`;
this.componentName = componentName;
if (this.isNodeParent) {
this._props = new Props(this, props);
this._directives = new Props(this, {});
Object.keys(extras).forEach(key => {
if (DIRECTIVES.indexOf(key) > -1) {
this.directives!.add((extras as any)[key], key);
delete (extras as any)[key];
}
});
this._extras = new Props(this, extras as any);
this._children = new NodeChildren(this as NodeParent, children || []);
} else {
this._children = new NodeContent(children);
}
}
/**
*
*/
get isNodeParent(): boolean {
return this.componentName.charAt(0) !== '#';
}
/**
* 使
*
* @ignore
*/
internalSetParent(parent: NodeParent | null) {
if (this._parent === parent) {
return;
}
if (this._parent) {
this._parent.children.delete(this);
}
this._parent = parent;
if (parent) {
this._zLevel = parent.zLevel + 1;
} else {
this._zLevel = -1;
}
}
/**
*
*/
remove() {
if (this.parent) {
this.parent.children.delete(this, true);
}
}
/**
*
*/
select() {
this.document.selection.select(this.id);
}
/**
*
*/
@obx.ref get component(): Component {
return this.document.getComponent(this.componentName);
}
/**
*
*/
@obx.ref get componentConfig(): ComponentConfig {
return this.document.getComponentConfig(this.component, this.componentName);
}
@obx.ref get propsData(): PropsMap | PropsList | null {
if (!this.isNodeParent || this.componentName === 'Fragment') {
return null;
}
return this.props?.value || null;
}
get directivesData(): PropsMap | null {
if (!this.isNodeParent) {
return null;
}
return this.directives?.value as PropsMap || null;
}
private _conditionGroup: string | null = null;
/**
*
*/
get conditionGroup(): string | null {
if (this._conditionGroup) {
return this._conditionGroup;
}
// 如果 condition 有值,且没有 group
if (this._condition) {
return this.id;
}
return null;
}
set conditionGroup(val) {
this._conditionGroup = val;
}
private _condition: any;
/**
*
*/
get condition() {
if (this._condition == null) {
if (this._conditionGroup) {
// FIXME: should be expression
return true;
}
return null;
}
return this._condition;
}
wrapWith(schema: NodeSchema) {
}
replaceWith(schema: NodeSchema, migrate: boolean = true) {
//
}
/*
// TODO
// 外部修改merge 进来,产生一次可恢复的历史数据
merge(data: ElementData) {
this.elementData = data;
const { leadingComments } = data;
this.leadingComments = leadingComments ? leadingComments.slice() : [];
this.parse();
this.mergeChildren(data.children || []);
}
// TODO: 再利用历史数据,不产生历史数据
reuse(timelineData: NodeSchema) {}
*/
getProp(path: string, useStash: boolean = true): Prop | null {
return this.props?.query(path, useStash as any) || null;
}
getDirective(name: string, useStash: boolean = true): Prop | null {
return this.directives?.get(name, useStash as any) || null;
}
/**
*
*/
get index(): number {
if (!this.parent) {
return -1;
}
return this.parent.children.indexOf(this);
}
/**
*
*/
get nextSibling(): Node | null {
if (!this.parent) {
return null;
}
const index = this.index;
if (index < 0) {
return null;
}
return this.parent.children.get(index + 1);
}
/**
*
*/
get prevSibling(): Node | null {
if (!this.parent) {
return null;
}
const index = this.index;
if (index < 1) {
return null;
}
return this.parent.children.get(index - 1);
}
/**
* - schema
*/
get schema(): NodeSchema {
// TODO: ..
return this.exportSchema(true);
}
/**
* schema
* @param serialize id
*/
exportSchema(serialize = false): NodeSchema {
// TODO...
const schema: any = {
componentName: this.componentName,
...this.extras,
props: this.props,
...this.directives,
};
if (serialize) {
schema.id = this.id;
}
if (isNodeParent(this)) {
if (this.children.size > 0) {
schema.children = this.children.exportSchema(serialize);
}
} else {
schema.children = (this.children as NodeContent).value;
}
return schema;
}
/**
*
*/
contains(node: Node): boolean {
return contains(this, node);
}
/**
*
*/
getZLevelTop(zLevel: number): Node | null {
return getZLevelTop(this, zLevel);
}
/**
*
*
* 16 thisNode contains otherNode
* 8 thisNode contained_by otherNode
* 2 thisNode before or after otherNode
* 0 thisNode same as otherNode
*/
comparePosition(otherNode: Node): number {
return comparePosition(this, otherNode);
}
private purged = false;
/**
*
*/
get isPurged() {
return this.purged;
}
/**
*
*/
purge() {
if (this.purged) {
return;
}
if (this._parent) {
// should remove thisNode before purge
this.remove();
return;
}
this.purged = true;
if (isNodeParent(this)) {
this.children.purge();
}
this.props?.purge();
this.directives?.purge();
this.extras?.purge();
this.document.internalRemoveAndPurgeNode(this);
}
}
export interface NodeParent extends Node {
readonly children: NodeChildren;
readonly props: Props<Node>;
readonly directives: Props<Node>;
readonly extras: Props<Node>;
}
export function isNode(node: any): node is Node {
return node && node.isNode;
}
export function isNodeParent(node: Node): node is NodeParent {
return node.isNodeParent;
}
export function getZLevelTop(child: Node, zLevel: number): Node | null {
let l = child.zLevel;
if (l < zLevel || zLevel < 0) {
return null;
}
if (l === zLevel) {
return child;
}
let r: any = child;
while (r && l-- > zLevel) {
r = r.parent;
}
return r;
}
export function contains(node1: Node, node2: Node): boolean {
if (node1 === node2) {
return true;
}
if (!node1.isNodeParent || !node2.parent) {
return false;
}
const p = getZLevelTop(node2, node1.zLevel);
if (!p) {
return false;
}
return node1 === p;
}
// 16 node1 contains node2
// 8 node1 contained_by node2
// 2 node1 before or after node2
// 0 node1 same as node2
export function comparePosition(node1: Node, node2: Node): number {
if (node1 === node2) {
return 0;
}
const l1 = node1.zLevel;
const l2 = node2.zLevel;
if (l1 === l2) {
return 2;
}
let p: any;
if (l1 > l2) {
p = getZLevelTop(node2, l1);
if (p && p === node1) {
return 16;
}
return 2;
}
p = getZLevelTop(node1, l2);
if (p && p === node2) {
return 8;
}
return 2;
}
export function insertChild(container: NodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node {
let node: Node;
if (copy && isNode(thing)) {
thing = thing.schema;
}
if (isNode(thing)) {
node = thing;
} else {
node = container.document.createNode(thing);
}
container.children.insert(node, at);
return node;
}
export function insertChildren(
container: NodeParent,
nodes: Node[] | NodeData[],
at?: number | null,
copy?: boolean,
): Node[] {
let index = at;
let node: any;
const results: Node[] = [];
// tslint:disable-next-line
while ((node = nodes.pop())) {
results.push(insertChild(container, node, index, copy));
index = node.index;
}
return results;
}

View File

@ -0,0 +1,453 @@
import { untracked, computed, obx } from '@recore/obx';
import { valueToSource } from '../../../../utils/value-to-source';
import { CompositeValue, isJSExpression } from '../../../schema';
import StashSpace from './stash-space';
import { uniqueId } from '../../../../utils/unique-id';
import { isPlainObject } from '../../../../utils/is-plain-object';
import { hasOwnProperty } from '../../../../utils/has-own-property';
export const UNSET = Symbol.for('unset');
export type UNSET = typeof UNSET;
export interface IPropParent {
delete(prop: Prop): void;
}
export default class Prop implements IPropParent {
readonly isProp = true;
readonly id = uniqueId('prop$');
private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' = 'unset';
/**
*
*/
get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' {
return this._type;
}
@obx.ref private _value: any = UNSET;
/**
*
*/
@computed get value(): CompositeValue {
if (this._type === 'unset') {
return null;
}
const type = this._type;
if (type === 'literal' || type === 'expression') {
return this._value;
}
if (type === 'map') {
if (!this._items) {
return this._value;
}
const maps: any = {};
this.items!.forEach((prop, key) => {
maps[key] = prop.value;
});
return maps;
}
if (type === 'list') {
if (!this._items) {
return this._items;
}
return this.items!.map(prop => prop.value);
}
return null;
}
/**
* set value, val should be JSON Object
*/
set value(val: CompositeValue) {
this._value = val;
const t = typeof val;
if (val == null) {
this._value = null;
this._type = 'literal';
} else if (t === 'string' || t === 'number' || t === 'boolean') {
this._value = val;
this._type = 'literal';
} else if (Array.isArray(val)) {
this._type = 'list';
} else if (isPlainObject(val)) {
if (isJSExpression(val)) {
this._type = 'expression';
} else {
this._type = 'map';
}
this._type = 'map';
} else {
this._type = 'expression';
this._value = {
type: 'JSExpression',
value: valueToSource(val),
};
}
if (untracked(() => this._items)) {
this._items!.forEach(prop => prop.purge());
this._items = null;
}
this._maps = null;
if (this.stash) {
this.stash.clear();
}
}
/**
*
*/
unset() {
this._type = 'unset';
}
/**
*
*/
isUnset() {
return this._type === 'unset';
}
/**
*
* JSExpresion | JSSlot
*/
isContainJSExpression(): boolean {
const type = this._type;
if (type === 'expression') {
return true;
}
if (type === 'literal' || type === 'unset') {
return false;
}
if ((type === 'list' || type === 'map') && this.items) {
return this.items.some(item => item.isContainJSExpression());
}
return false;
}
/**
* JSON
*/
isJSON() {
return !this.isContainJSExpression();
}
private _items: Prop[] | null = null;
private _maps: Map<string, Prop> | null = null;
@computed private get items(): Prop[] | null {
let _items: any;
untracked(() => {
_items = this._items;
});
if (!_items) {
if (this._type === 'list') {
const data = this._value;
const items = [];
for (const item of data) {
items.push(new Prop(this, item));
}
_items = items;
this._maps = null;
} else if (this._type === 'map') {
const data = this._value;
const items = [];
const maps = new Map<string, Prop>();
const keys = Object.keys(data);
for (const key of keys) {
const prop = new Prop(this, data[key], key);
items.push(prop);
maps.set(key, prop);
}
_items = items;
this._maps = maps;
} else {
_items = null;
this._maps = null;
}
this._items = _items;
}
return _items;
}
@computed private get maps(): Map<string, Prop> | null {
if (!this.items || this.items.length < 1) {
return null;
}
return this._maps;
}
private stash: StashSpace | undefined;
/**
*
*/
@obx key: string | number | undefined;
/**
*
*/
@obx spread: boolean;
constructor(
public parent: IPropParent,
value: CompositeValue | UNSET = UNSET,
key?: string | number,
spread = false,
) {
if (value !== UNSET) {
this.value = value;
}
this.key = key;
this.spread = spread;
}
/**
*
* @param stash
*/
get(path: string, stash: false): Prop | null;
/**
* ,
* @param stash
*/
get(path: string, stash: true): Prop;
/**
* ,
*/
get(path: string): Prop;
get(path: string, stash = true) {
const type = this._type;
if (type !== 'map' && type !== 'unset' && !stash) {
return null;
}
const maps = type === 'map' ? this.maps : null;
let prop: any = maps ? maps.get(path) : null;
if (prop) {
return prop;
}
const i = path.indexOf('.');
let entry = path;
let nest = '';
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
prop = maps ? maps.get(entry) : null;
if (prop) {
return prop.get(nest, stash);
}
}
}
if (stash) {
if (!this.stash) {
this.stash = new StashSpace(
item => {
// item take effect
this.set(String(item.key), item);
item.parent = this;
},
() => {
return true;
},
);
}
prop = this.stash.get(entry);
if (nest) {
return prop.get(nest, true);
}
return prop;
}
return null;
}
/**
*
*/
remove() {
this.parent.delete(this);
}
/**
*
*/
delete(prop: Prop): void {
if (this.items) {
const i = this.items.indexOf(prop);
if (i > -1) {
this.items.slice(i, 1);
prop.purge();
}
if (this._maps && prop.key) {
this._maps.delete(String(prop.key));
}
}
}
/**
* key
*/
deleteKey(key: string): void {
if (this.maps) {
const prop = this.maps.get(key);
if (prop) {
this.delete(prop);
}
}
}
/**
*
*/
size(): number {
return this.items?.length || 0;
}
/**
*
*
* @param force
*/
add(value: CompositeValue, force = false): Prop | null {
const type = this._type;
if (type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'list')) {
this.value = [];
}
const prop = new Prop(this, value);
this.items!.push(prop);
return prop;
}
/**
*
*
* @param force
*/
set(key: string, value: CompositeValue | Prop, force = false) {
const type = this._type;
if (type !== 'map' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'map')) {
this.value = {};
}
const prop = isProp(value) ? value : new Prop(this, value, key);
const items = this.items!;
const maps = this.maps!;
const orig = maps.get(key);
if (orig) {
// replace
const i = items.indexOf(orig);
if (i > -1) {
items.splice(i, 1, prop)[0].purge();
}
maps.set(key, prop);
} else {
// push
items.push(prop);
maps.set(key, prop);
}
return prop;
}
/**
* key
*/
has(key: string): boolean {
if (this._type !== 'map') {
return false;
}
if (this._maps) {
return this._maps.has(key);
}
return hasOwnProperty(this._value, key);
}
private purged = false;
/**
*
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
if (this.stash) {
this.stash.purge();
}
if (this._items) {
this._items.forEach(item => item.purge());
}
this._maps = null;
}
/**
*
*/
[Symbol.iterator](): { next(): { value: Prop } } {
let index = 0;
const items = this.items;
const length = items?.length || 0;
return {
next() {
if (index < length) {
return {
value: items![index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
*
*/
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
const items = this.items;
if (!items) {
return;
}
const isMap = this._type === 'map';
items.forEach((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
/**
*
*/
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
const items = this.items;
if (!items) {
return null;
}
const isMap = this._type === 'map';
return items.map((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
}
export function isProp(obj: any): obj is Prop {
return obj && obj.isProp;
}

View File

@ -0,0 +1,242 @@
import { computed, obx } from '@recore/obx';
import { uniqueId } from '../../../../utils/unique-id';
import { CompositeValue, PropsList, PropsMap } from '../../../schema';
import StashSpace from './stash-space';
import Prop, { IPropParent } from './prop';
export const UNSET = Symbol.for('unset');
export type UNSET = typeof UNSET;
export default class Props<O = any> implements IPropParent {
readonly id = uniqueId('props');
@obx.val private items: Prop[] = [];
@obx.ref private get maps(): Map<string, Prop> {
const maps = new Map();
if (this.items.length > 0) {
this.items.forEach(prop => {
if (prop.key) {
maps.set(prop.key, prop);
}
});
}
return maps;
}
private stash = new StashSpace(
prop => {
this.items.push(prop);
prop.parent = this;
},
() => {
return true;
},
);
/**
*
*/
get size() {
return this.items.length;
}
@computed get value(): PropsMap | PropsList | null {
if (this.items.length < 1) {
return null;
}
if (this.type === 'list') {
return this.items.map(item => ({
spread: item.spread,
name: item.key as string,
value: item.value,
}));
}
const maps: any = {};
this.items.forEach(prop => {
if (prop.key) {
maps[prop.key] = prop.value;
}
});
return maps;
}
@obx type: 'map' | 'list' = 'map';
constructor(readonly owner: O, value?: PropsMap | PropsList | null) {
if (Array.isArray(value)) {
this.type = 'list';
value.forEach(item => {});
} else if (value != null) {
this.type = 'map';
}
}
/**
* path
*/
query(path: string): Prop;
/**
* path
*
* @useStash
*/
query(path: string, useStash: true): Prop;
/**
* path
*/
query(path: string, useStash: false): Prop | null;
/**
* path
*
* @useStash
*/
query(path: string, useStash: boolean = true) {
let matchedLength = 0;
let firstMatched = null;
if (this.items) {
// target: a.b.c
// trys: a.b.c, a.b, a
let i = this.items.length;
while (i-- > 0) {
const expr = this.items[i];
if (!expr.key) {
continue;
}
const name = String(expr.key);
if (name === path) {
// completely match
return expr;
}
// fisrt match
const l = name.length;
if (path.slice(0, l + 1) === `${name}.`) {
matchedLength = l;
firstMatched = expr;
}
}
}
let ret = null;
if (firstMatched) {
ret = firstMatched.get(path.slice(matchedLength + 1), true);
}
if (!ret && useStash) {
return this.stash.get(path);
}
return ret;
}
/**
* ,
* @param useStash
*/
get(path: string, useStash: true): Prop;
/**
*
* @param useStash
*/
get(path: string, useStash: false): Prop | null;
/**
*
*/
get(path: string): Prop | null;
get(name: string, useStash = false) {
return this.maps.get(name) || (useStash && this.stash.get(name)) || null;
}
/**
*
*/
delete(prop: Prop): void {
const i = this.items.indexOf(prop);
if (i > -1) {
this.items.splice(i, 1);
prop.purge();
}
}
/**
* key
*/
deleteKey(key: string): void {
this.items = this.items.filter(item => {
if (item.key === key) {
item.purge();
return false;
}
return true;
});
}
/**
*
*/
add(value: CompositeValue | null, key?: string | number, spread = false): Prop {
const prop = new Prop(this, value, key, spread);
this.items.push(prop);
return prop;
}
/**
* key
*/
has(key: string): boolean {
return this.maps.has(key);
}
/**
*
*/
[Symbol.iterator](): { next(): { value: Prop } } {
let index = 0;
const items = this.items;
const length = items.length || 0;
return {
next() {
if (index < length) {
return {
value: items[index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
*
*/
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
this.items.forEach(item => {
return fn(item, item.key);
});
}
/**
*
*/
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
return this.items.map(item => {
return fn(item, item.key);
});
}
private purged = false;
/**
*
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.stash.purge();
this.items.forEach(item => item.purge());
}
}

View File

@ -0,0 +1,65 @@
import { obx, autorun, untracked } from '@recore/obx';
import Prop, { IPropParent } from './prop';
export type PendingItem = Prop[];
export default class StashSpace implements IPropParent {
@obx.val private space: Set<Prop> = new Set();
@obx.ref private get maps(): Map<string, Prop> {
const maps = new Map();
if (this.space.size > 0) {
this.space.forEach(prop => {
maps.set(prop.key, prop);
});
}
return maps;
}
private willPurge: () => void;
constructor(write: (item: Prop) => void, before: () => boolean) {
this.willPurge = autorun(() => {
if (this.space.size < 1) {
return;
}
const pending: Prop[] = [];
for (const prop of this.space) {
if (!prop.isUnset()) {
this.space.delete(prop);
pending.push(prop);
}
}
if (pending.length > 0) {
untracked(() => {
if (before()) {
for (const item of pending) {
write(item);
}
}
});
}
});
}
get(key: string): Prop {
let prop = this.maps.get(key);
if (!prop) {
prop = new Prop(this, null, key);
this.space.add(prop);
}
return prop;
}
delete(prop: Prop) {
this.space.delete(prop);
prop.purge();
}
clear() {
this.space.forEach(item => item.purge());
this.space.clear();
}
purge() {
this.willPurge();
this.space.clear();
}
}

View File

@ -0,0 +1,77 @@
import Node, { NodeParent } from './node';
import { RootSchema } from '../../schema';
import DocumentModel from '../document-model';
import NodeChildren from './node-children';
import Props from './props/props';
/**
*
*
* [Node Properties]
* componentName: Page/Block/Component
* props
* children
*
* [Root Container Extra Properties]
* fileName
* meta
* state
* defaultProps
* lifeCycles
* methods
* dataSource
* css
*
* [Directives **not used**]
* loop
* loopArgs
* condition
* ------- future support -----
* conditionGroup
* title
* ignore
* locked
* hidden
*/
export default class RootNode extends Node implements NodeParent {
readonly isRootNode = true;
readonly isNodeParent = true;
readonly index = 0;
readonly nextSibling = null;
readonly prevSibling = null;
readonly zLevel = 0;
readonly parent = null;
get children(): NodeChildren {
return this._children as NodeChildren;
}
get props(): Props<RootNode> {
return this._props as any;
}
get extras(): Props<RootNode> {
return this._extras as any;
}
get directives(): Props<RootNode> {
return this._props as any;
}
internalSetParent(parent: null) {}
constructor(readonly document: DocumentModel, rootSchema: RootSchema) {
super(document, rootSchema);
}
isPage() {
return this.componentName === 'Page';
}
isComponent() {
return this.componentName === 'Component';
}
isBlock() {
return this.componentName === 'Block';
}
}
export function isRootNode(node: any): node is RootNode {
return node && node.isRootNode;
}

View File

@ -168,17 +168,14 @@ export default class Dragon {
}
getMasterSensors(): ISimulator[] {
return this.designer.project.documents.map(doc => (doc.actived && doc.simulator) || null).filter(Boolean);
}
get master(): DocumentModel {
return this.designer.project.documents.map(doc => (doc.actived && doc.simulator) || null).filter(Boolean) as any;
}
/**
* dragTarget should be a INode | INode[] | NodeData | NodeData[]
*/
boost(dragObject: DragObject, boostEvent: MouseEvent) {
/*
const doc = document;
const fromTop = isFromTopDocument(boostEvent);
let lastLocation: any = null;
@ -368,6 +365,7 @@ export default class Dragon {
topDoc.addEventListener('keyup', checkcopy as any, false);
}
}
*/
}
addSensor(sensor: any) {

View File

@ -0,0 +1 @@
// todo

View File

@ -0,0 +1,34 @@
import { obx } from '@recore/obx';
import Node from './document/node/node';
import DocumentModel from './document/document-model';
export default class Hovering {
@obx.ref private _enable: boolean = true;
get enable() {
return this._enable;
}
set enable(flag: boolean) {
this._enable = flag;
if (!flag) {
this._hovering = null;
}
}
@obx.ref xRayMode: boolean = false;
@obx.ref private _hovering: Node | null = null;
get hovering() {
return this._hovering;
}
@obx.ref event?: MouseEvent;
hover(node: Node | null, e: MouseEvent) {
this._hovering = node;
this.event = e;
}
leave(document: DocumentModel) {
if (this.hovering && this.hovering.document === document) {
this._hovering = null;
}
}
}

View File

@ -0,0 +1,3 @@
import DesignerView from './designer-view';
export default DesignerView;

View File

@ -0,0 +1,126 @@
import ComponentNode, { NodeParent } from './document/node/node';
import DocumentModel from './document/document-model';
export interface LocationData {
target: NodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode
detail: LocationDetail;
}
export enum LocationDetailType {
Children = 'Children',
Prop = 'Prop',
}
export interface LocationChildrenDetail {
type: LocationDetailType.Children;
index: number;
near?: {
node: ComponentNode;
pos: 'before' | 'after';
rect?: Rect;
align?: 'V' | 'H';
};
}
export interface LocationPropDetail {
// cover 形态,高亮 domNode如果 domNode 为空,取 container 的值
type: LocationDetailType.Prop;
name: string;
domNode?: HTMLElement;
}
export type LocationDetail = LocationChildrenDetail | LocationPropDetail | { type: string; [key: string]: any };
export interface Point {
clientX: number;
clientY: number;
}
export type Rects = Array<ClientRect | DOMRect> & {
elements: Array<Element | Text>;
};
export type Rect = (ClientRect | DOMRect) & {
elements: Array<Element | Text>;
computed?: boolean;
};
export function isLocationData(obj: any): obj is LocationData {
return obj && obj.target && obj.detail;
}
export function isLocationChildrenDetail(obj: any): obj is LocationChildrenDetail {
return obj && obj.type === LocationDetailType.Children;
}
export function isRowContainer(container: Element | Text, win?: Window) {
if (isText(container)) {
return true;
}
const style = (win || getWindow(container)).getComputedStyle(container);
const display = style.getPropertyValue('display');
if (/flex$/.test(display)) {
const direction = style.getPropertyValue('flex-direction') || 'row';
if (direction === 'row' || direction === 'row-reverse') {
return true;
}
}
return false;
}
export function isChildInline(child: Element | Text, win?: Window) {
if (isText(child)) {
return true;
}
const style = (win || getWindow(child)).getComputedStyle(child);
return /^inline/.test(style.getPropertyValue('display'));
}
export function getRectTarget(rect: Rect | null) {
if (!rect || rect.computed) {
return null;
}
const els = rect.elements;
return els && els.length > 0 ? els[0]! : null;
}
export function isVerticalContainer(rect: Rect | null) {
const el = getRectTarget(rect);
if (!el) {
return false;
}
return isRowContainer(el);
}
export function isVertical(rect: Rect | null) {
const el = getRectTarget(rect);
if (!el) {
return false;
}
return isChildInline(el) || (el.parentElement ? isRowContainer(el.parentElement) : false);
}
function isText(elem: any): elem is Text {
return elem.nodeType === Node.TEXT_NODE;
}
function isDocument(elem: any): elem is Document {
return elem.nodeType === Node.DOCUMENT_NODE;
}
export function getWindow(elem: Element | Document): Window {
return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!;
}
export default class Location {
readonly target: NodeParent;
readonly detail: LocationDetail;
get document(): DocumentModel {
return this.target.document;
}
constructor({ target, detail }: LocationData) {
this.target = target;
this.detail = detail;
}
}

View File

@ -0,0 +1,22 @@
import { Component } from 'react';
import { observer } from '@recore/core-obx';
import Designer from './designer';
import DocumentView from './document/document-view';
@observer
export default class ProjectView extends Component<{ designer: Designer }> {
render() {
const { designer } = this.props;
// TODO: support splitview
return (
<div className="lc-project">
{designer.project.documents.map(doc => {
if (!doc.opened) {
return null;
}
return <DocumentView key={doc.id} document={doc} />;
})}
</div>
);
}
}

View File

@ -31,7 +31,7 @@ export interface CompositeObject {
}
export interface NpmInfo {
componentName: string;
componentName?: string;
package: string;
version: string;
destructuring?: boolean;

View File

@ -0,0 +1,181 @@
import { isElement } from '../utils/dom';
export class ScrollTarget {
get left() {
return 'scrollX' in this.target ? this.target.scrollX : this.target.scrollLeft;
}
get top() {
return 'scrollY' in this.target ? this.target.scrollY : this.target.scrollTop;
}
scrollTo(options: { left?: number; top?: number }) {
this.target.scrollTo(options);
}
scrollToXY(x: number, y: number) {
this.target.scrollTo(x, y);
}
get scrollHeight(): number {
return ((this.doe || this.target) as any).scrollHeight;
}
get scrollWidth(): number {
return ((this.doe || this.target) as any).scrollWidth;
}
private doe?: HTMLElement;
constructor(private target: Window | Element) {
if (isWindow(target)) {
this.doe = target.document.documentElement;
}
}
}
function isWindow(obj: any): obj is Window {
return obj && obj.document;
}
function easing(n: number) {
return Math.sin((n * Math.PI) / 2);
}
const SCROLL_ACCURCY = 30;
export interface IScrollable {
scrollTarget?: ScrollTarget | Element;
bounds: DOMRect;
scale: number;
}
export default class Scroller {
private pid: number | undefined;
get scrollTarget(): ScrollTarget | null {
let target = this.scrollable.scrollTarget;
if (!target) {
return null;
}
if (isElement(target)) {
target = new ScrollTarget(target);
this.scrollable.scrollTarget = target;
}
return target;
}
constructor(private scrollable: IScrollable) {
}
scrollTo(options: { left?: number; top?: number }) {
this.cancel();
const scrollTarget = this.scrollTarget;
if (!scrollTarget) {
return;
}
let pid: number;
const left = scrollTarget.left;
const top = scrollTarget.top;
const end = () => {
this.cancel();
};
if ((left === options.left || options.left == null) && top === options.top) {
end();
return;
}
const duration = 200;
const start = +new Date();
const animate = () => {
if (pid !== this.pid) {
return;
}
const now = +new Date();
const time = Math.min(1, (now - start) / duration);
const eased = easing(time);
const opt: any = {};
if (options.left != null) {
opt.left = eased * (options.left - left) + left;
}
if (options.top != null) {
opt.top = eased * (options.top - top) + top;
}
scrollTarget.scrollTo(opt);
if (time < 1) {
this.pid = pid = requestAnimationFrame(animate);
} else {
end();
}
};
this.pid = pid = requestAnimationFrame(animate);
}
scrolling(point: { globalX: number; globalY: number }) {
this.cancel();
const { bounds, scale } = this.scrollable;
const scrollTarget = this.scrollTarget;
if (!scrollTarget) {
return;
}
const x = point.globalX;
const y = point.globalY;
const maxScrollHeight = scrollTarget.scrollHeight - bounds.height / scale;
const maxScrollWidth = scrollTarget.scrollWidth - bounds.width / scale;
let sx = scrollTarget.left;
let sy = scrollTarget.top;
let ax = 0;
let ay = 0;
if (y < bounds.top + SCROLL_ACCURCY) {
ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale;
} else if (y > bounds.bottom - SCROLL_ACCURCY) {
ay = Math.min(Math.max(y + SCROLL_ACCURCY - bounds.bottom, 10), 50) / scale;
}
if (x < bounds.left + SCROLL_ACCURCY) {
ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale;
} else if (x > bounds.right - SCROLL_ACCURCY) {
ax = Math.min(Math.max(x + SCROLL_ACCURCY - bounds.right, 10), 50) / scale;
}
if (!ax && !ay) {
return;
}
const animate = () => {
let scroll = false;
if ((ay > 0 && sy < maxScrollHeight) || (ay < 0 && sy > 0)) {
sy += ay;
sy = Math.min(Math.max(sy, 0), maxScrollHeight);
scroll = true;
}
if ((ax > 0 && sx < maxScrollWidth) || (ax < 0 && sx > 0)) {
sx += ax;
sx = Math.min(Math.max(sx, 0), maxScrollWidth);
scroll = true;
}
if (!scroll) {
return;
}
scrollTarget.scrollTo({ left: sx, top: sy });
this.pid = requestAnimationFrame(animate);
};
animate();
}
cancel() {
if (this.pid) {
cancelAnimationFrame(this.pid);
}
this.pid = undefined;
}
}

View File

@ -0,0 +1,156 @@
import { Component as ReactComponent, ComponentType } from 'react';
import { LocateEvent, ISensor } from './dragon';
import { Point } from './location';
import Node from './document/node/node';
import { ScrollTarget, IScrollable } from './scroller';
import { ComponentDescriptionSpec } from './document/node/component-config';
export type AutoFit = '100%';
export const AutoFit = '100%';
export interface IViewport extends IScrollable {
/**
*
*/
width: number;
height: number;
/**
*
*/
contentWidth: number | AutoFit;
contentHeight: number | AutoFit;
/**
*
*/
scale: number;
/**
*
*/
readonly bounds: DOMRect;
/**
*
*/
readonly contentBounds: DOMRect;
readonly scrollTarget?: ScrollTarget;
/**
* X
*/
readonly scrollX: number;
/**
* Y
*/
readonly scrollY: number;
/**
*
*/
toLocalPoint(point: Point): Point;
/**
*
*/
toGlobalPoint(point: Point): Point;
}
/**
*
*/
export interface ISimulator<P = object> extends ISensor {
/**
*
*/
readonly viewport: IViewport;
readonly contentWindow?: Window;
readonly contentDocument?: Document;
// dependsAsset // like react jQuery lodash
// themesAsset
// componentsAsset
// simulatorUrl //
// utils, dataSource, constants 模拟
//
// later:
// layout: ComponentName
// 获取区块代码, 通过 components 传递,可异步获取
setProps(props: P): void;
/**
*
*/
setDraggingState(state: boolean): void;
/**
*
*/
isDraggingState(): boolean;
/**
*
*/
setCopyState(state: boolean): void;
/**
*
*/
isCopyState(): boolean;
/**
*
*/
clearState(): void;
/**
*
*/
locate(e: LocateEvent): any;
/**
*
*/
scrollToNode(node: Node, detail?: any): void;
/**
* event canvasX, globalX
*/
fixEvent(e: LocateEvent): LocateEvent;
/**
*
*/
describeComponent(component: Component): ComponentDescriptionSpec;
/**
*
*/
getComponent(componentName: string): Component | any;
/**
*
*/
getComponentInstance(node: Node): ComponentInstance[] | null;
/**
*
*/
getComponentContext(node: Node): object;
getClosestNodeId(elem: Element): string;
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null;
setSuspense(suspensed: boolean): void;
/**
*
*/
purge(): void;
}
/**
*
*/
export type Component = ComponentType<any> | object;
/**
*
*/
export type ComponentInstance = Element | ReactComponent<any> | object;

View File

@ -0,0 +1,3 @@
import DesignerView from './designer';
export default DesignerView;

1
packages/designer/src/module.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '@ali/iceluna-sdk';

View File

@ -1,9 +0,0 @@
export * from './is-object';
export * from './is-plain-object';
export * from './has-own-property';
export * from './set-prototype-of';
export * from './get-prototype-of';
export * from './shallow-equal';
export * from './clone-deep';
export * from './throttle';
export * from './unique-id';

View File

@ -0,0 +1,15 @@
let nativeSelectionEnabled = true;
const preventSelection = (e: Event) => {
if (nativeSelectionEnabled) {
return null;
}
e.preventDefault();
e.stopPropagation();
return false;
};
document.addEventListener('selectstart', preventSelection, true);
document.addEventListener('dragstart', preventSelection, true);
export function setNativeSelection(enableFlag: boolean) {
nativeSelectionEnabled = enableFlag;
}

View File

@ -1,7 +0,0 @@
export function parseCode(code: string): string {
try {
return JSON.parse(code);
} catch (e) {
return code;
}
}

View File

@ -1,27 +0,0 @@
import { hasOwnProperty } from './has-own-property';
export function shallowEqual(objA: any, objB: any): boolean {
if (objA === objB) {
return true;
}
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (!hasOwnProperty(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
return false;
}
}
return true;
}

View File

@ -1,55 +0,0 @@
export default class StylePoint {
private lastContent: string | undefined;
private lastUrl: string | undefined;
placeholder: Element | Text;
next: StylePoint | null = null;
prev: StylePoint | null = null;
constructor(readonly id: string, readonly level: number, placeholder?: Element) {
if (placeholder) {
this.placeholder = placeholder;
} else {
this.placeholder = document.createTextNode('');
}
}
insert() {
if (this.next) {
document.head.insertBefore(this.placeholder, this.next.placeholder);
} else if (this.prev) {
document.head.insertBefore(this.placeholder, this.prev.placeholder.nextSibling);
} else {
document.head.appendChild(this.placeholder);
}
}
applyText(content: string) {
if (this.lastContent === content) {
return;
}
this.lastContent = content;
this.lastUrl = undefined;
const element = document.createElement('style');
element.setAttribute('type', 'text/css');
element.setAttribute('data-for', this.id);
element.appendChild(document.createTextNode(content));
document.head.insertBefore(element, this.placeholder);
document.head.removeChild(this.placeholder);
this.placeholder = element;
}
applyUrl(url: string) {
if (this.lastUrl === url) {
return;
}
this.lastContent = undefined;
this.lastUrl = url;
const element = document.createElement('link');
element.href = url;
element.rel = 'stylesheet';
element.setAttribute('data-for', this.id);
document.head.insertBefore(element, this.placeholder);
document.head.removeChild(this.placeholder);
this.placeholder = element;
}
}

View File

@ -1,11 +0,0 @@
import { Component, isValidElement } from 'react';
export function testReactType(obj: any) {
const t = typeof obj;
if (t === 'function' && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component)) {
return 'ReactClass';
} else if (t === 'object' && isValidElement(obj)) {
return 'ReactElement';
}
return t;
}