mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-29 08:40:56 +00:00
debug start
This commit is contained in:
parent
a12aa43c64
commit
28d23d5d78
39
packages/designer/auxilary/gliding.less
Normal file
39
packages/designer/auxilary/gliding.less
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
packages/designer/auxilary/gliding.tsx
Normal file
47
packages/designer/auxilary/gliding.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
82
packages/designer/src/builtins/drag-ghost/index.tsx
Normal file
82
packages/designer/src/builtins/drag-ghost/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
1
packages/designer/src/builtins/simulator/host/README.md
Normal file
1
packages/designer/src/builtins/simulator/host/README.md
Normal file
@ -0,0 +1 @@
|
||||
主进程
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
95
packages/designer/src/builtins/simulator/host/host-view.tsx
Normal file
95
packages/designer/src/builtins/simulator/host/host-view.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
67
packages/designer/src/builtins/simulator/host/host.less
Normal file
67
packages/designer/src/builtins/simulator/host/host.less
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
799
packages/designer/src/builtins/simulator/host/host.ts
Normal file
799
packages/designer/src/builtins/simulator/host/host.ts
Normal 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
|
||||
}
|
||||
2
packages/designer/src/builtins/simulator/host/index.ts
Normal file
2
packages/designer/src/builtins/simulator/host/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './host';
|
||||
export * from './host-view';
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
135
packages/designer/src/builtins/simulator/host/viewport.ts
Normal file
135
packages/designer/src/builtins/simulator/host/viewport.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
5
packages/designer/src/builtins/simulator/index.ts
Normal file
5
packages/designer/src/builtins/simulator/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SimulatorHostView } from './host/host-view';
|
||||
|
||||
export * from './host/host';
|
||||
export * from './host/host-view';
|
||||
export default SimulatorHostView;
|
||||
@ -0,0 +1 @@
|
||||
沙箱环境
|
||||
@ -0,0 +1,4 @@
|
||||
// NOTE: 仅做类型标注,切勿做其它用途
|
||||
import { SimulatorHost } from '../host';
|
||||
|
||||
export const host: SimulatorHost = (window as any).LCSimulatorHost;
|
||||
@ -0,0 +1,7 @@
|
||||
import renderer from './renderer';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).SimulatorRenderer = renderer;
|
||||
}
|
||||
|
||||
export default renderer;
|
||||
@ -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>
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
html {
|
||||
padding-bottom: 30px;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body, html {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
background: white;
|
||||
}
|
||||
259
packages/designer/src/builtins/simulator/renderer/renderer.ts
Normal file
259
packages/designer/src/builtins/simulator/renderer/renderer.ts
Normal 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();
|
||||
76
packages/designer/src/builtins/simulator/utils/asset.ts
Normal file
76
packages/designer/src/builtins/simulator/utils/asset.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
105
packages/designer/src/builtins/simulator/utils/loader.ts
Normal file
105
packages/designer/src/builtins/simulator/utils/loader.ts
Normal 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();
|
||||
@ -1,5 +1,5 @@
|
||||
import { ReactInstance } from 'react';
|
||||
import { isDOMNode, isElement } from './dom';
|
||||
import { isDOMNode, isElement } from '../../../utils/dom';
|
||||
|
||||
const FIBER_KEY = '_reactInternalFiber';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
73
packages/designer/src/builtins/simulator/utils/style.ts
Normal file
73
packages/designer/src/builtins/simulator/utils/style.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
198
packages/designer/src/builtins/simulator/utils/vision-types.js
Normal file
198
packages/designer/src/builtins/simulator/utils/vision-types.js
Normal 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
|
||||
23
packages/designer/src/designer/active-tracker.ts
Normal file
23
packages/designer/src/designer/active-tracker.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
51
packages/designer/src/designer/designer-view.tsx
Normal file
51
packages/designer/src/designer/designer-view.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
0
packages/designer/src/designer/designer.less
Normal file
0
packages/designer/src/designer/designer.less
Normal 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';
|
||||
|
||||
349
packages/designer/src/designer/document/document-model.ts
Normal file
349
packages/designer/src/designer/document/document-model.ts
Normal 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;
|
||||
}
|
||||
35
packages/designer/src/designer/document/document-view.tsx
Normal file
35
packages/designer/src/designer/document/document-view.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
380
packages/designer/src/designer/document/node/component-config.ts
Normal file
380
packages/designer/src/designer/document/node/component-config.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
186
packages/designer/src/designer/document/node/node-children.ts
Normal file
186
packages/designer/src/designer/document/node/node-children.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
86
packages/designer/src/designer/document/node/node-content.ts
Normal file
86
packages/designer/src/designer/document/node/node-content.ts
Normal 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 === '';
|
||||
}
|
||||
}
|
||||
481
packages/designer/src/designer/document/node/node.ts
Normal file
481
packages/designer/src/designer/document/node/node.ts
Normal 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;
|
||||
}
|
||||
|
||||
453
packages/designer/src/designer/document/node/props/prop.ts
Normal file
453
packages/designer/src/designer/document/node/props/prop.ts
Normal 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;
|
||||
}
|
||||
242
packages/designer/src/designer/document/node/props/props.ts
Normal file
242
packages/designer/src/designer/document/node/props/props.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
77
packages/designer/src/designer/document/node/root-node.ts
Normal file
77
packages/designer/src/designer/document/node/root-node.ts
Normal 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;
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
1
packages/designer/src/designer/history.ts
Normal file
1
packages/designer/src/designer/history.ts
Normal file
@ -0,0 +1 @@
|
||||
// todo
|
||||
34
packages/designer/src/designer/hovering.ts
Normal file
34
packages/designer/src/designer/hovering.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import DesignerView from './designer-view';
|
||||
|
||||
export default DesignerView;
|
||||
126
packages/designer/src/designer/location.ts
Normal file
126
packages/designer/src/designer/location.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
22
packages/designer/src/designer/project-view.tsx
Normal file
22
packages/designer/src/designer/project-view.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -31,7 +31,7 @@ export interface CompositeObject {
|
||||
}
|
||||
|
||||
export interface NpmInfo {
|
||||
componentName: string;
|
||||
componentName?: string;
|
||||
package: string;
|
||||
version: string;
|
||||
destructuring?: boolean;
|
||||
|
||||
181
packages/designer/src/designer/scroller.ts
Normal file
181
packages/designer/src/designer/scroller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
156
packages/designer/src/designer/simulator.ts
Normal file
156
packages/designer/src/designer/simulator.ts
Normal 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;
|
||||
@ -0,0 +1,3 @@
|
||||
import DesignerView from './designer';
|
||||
|
||||
export default DesignerView;
|
||||
1
packages/designer/src/module.d.ts
vendored
Normal file
1
packages/designer/src/module.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '@ali/iceluna-sdk';
|
||||
@ -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';
|
||||
15
packages/designer/src/utils/navtive-selection.ts
Normal file
15
packages/designer/src/utils/navtive-selection.ts
Normal 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;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export function parseCode(code: string): string {
|
||||
try {
|
||||
return JSON.parse(code);
|
||||
} catch (e) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user