mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-13 20:36:34 +00:00
1108 lines
29 KiB
TypeScript
1108 lines
29 KiB
TypeScript
import { obx, autorun, computed } from '@ali/lowcode-globals';
|
|
import { ISimulatorHost, Component, NodeInstance, ComponentInstance } from '../simulator';
|
|
import Viewport from './viewport';
|
|
import { createSimulator } from './create-simulator';
|
|
import { Node, NodeParent, DocumentModel, isNodeParent, isNode, contains, isRootNode } from '../document';
|
|
import ResourceConsumer from './resource-consumer';
|
|
import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType, getPublicPath } from '@ali/lowcode-globals';
|
|
import {
|
|
DragObjectType,
|
|
isShaken,
|
|
LocateEvent,
|
|
isDragAnyObject,
|
|
isDragNodeObject,
|
|
LocationData,
|
|
isLocationData,
|
|
LocationChildrenDetail,
|
|
LocationDetailType,
|
|
isChildInline,
|
|
isRowContainer,
|
|
getRectTarget,
|
|
Rect,
|
|
CanvasPoint,
|
|
} from '../designer';
|
|
import { parseProps } from './utils/parse-props';
|
|
import { isElement } from '@ali/lowcode-globals';
|
|
import { ComponentMetadata } from '@ali/lowcode-globals';
|
|
import { BuiltinSimulatorRenderer } from './renderer';
|
|
|
|
export interface LibraryItem {
|
|
package: string;
|
|
library: string;
|
|
urls: Asset;
|
|
}
|
|
|
|
export interface BuiltinSimulatorProps {
|
|
// 从 documentModel 上获取
|
|
// suspended?: boolean;
|
|
designMode?: 'live' | 'design' | 'mock' | 'extend' | 'border' | 'preview';
|
|
device?: 'mobile' | 'iphone' | string;
|
|
deviceClassName?: string;
|
|
simulatorUrl?: Asset;
|
|
environment?: Asset;
|
|
library?: LibraryItem[];
|
|
theme?: Asset;
|
|
componentsAsset?: Asset;
|
|
[key: string]: any;
|
|
}
|
|
|
|
const defaultSimulatorUrl = (() => {
|
|
const publicPath = getPublicPath();
|
|
let urls;
|
|
const [_, prefix = '', dev] = /^(.+?)(\/js)?\/?$/.exec(publicPath) || [];
|
|
if (dev) {
|
|
urls = [`${prefix}/css/react-simulator-renderer.css`, `${prefix}/js/react-simulator-renderer.js`];
|
|
} else if (process.env.NODE_ENV === 'production') {
|
|
urls = [`${prefix}/react-simulator-renderer.min.css`, `${prefix}/react-simulator-renderer.min.js`];
|
|
} else {
|
|
urls = [`${prefix}/react-simulator-renderer.css`, `${prefix}/react-simulator-renderer.js`];
|
|
}
|
|
return urls;
|
|
})();
|
|
|
|
const defaultEnvironment = [
|
|
// 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
|
|
assetItem(AssetType.JSText, 'window.React=parent.React;window.ReactDOM=parent.ReactDOM;', undefined, 'react'),
|
|
assetItem(
|
|
AssetType.JSText,
|
|
'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;',
|
|
),
|
|
];
|
|
|
|
export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProps> {
|
|
readonly isSimulator = true;
|
|
|
|
constructor(readonly document: DocumentModel) {}
|
|
|
|
readonly designer = this.document.designer;
|
|
|
|
@computed get device(): string | undefined {
|
|
// 根据 device 不同来做画布外框样式变化 渲染时可选择不同组件
|
|
// renderer 依赖
|
|
return this.get('device') || 'default';
|
|
}
|
|
|
|
@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 theme(): Asset | undefined {
|
|
return this.get('theme');
|
|
}
|
|
|
|
@computed get componentsMap() {
|
|
// renderer 依赖
|
|
return this.designer.componentsMap;
|
|
}
|
|
|
|
@obx.ref _props: BuiltinSimulatorProps = {};
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
setProps(props: BuiltinSimulatorProps) {
|
|
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: BuiltinSimulatorRenderer, fn: (context: { dispose: () => void; firstRun: boolean }) => void) {
|
|
this._renderer = renderer;
|
|
return autorun(fn as any, true);
|
|
}
|
|
|
|
purge(): void {
|
|
// todo
|
|
}
|
|
|
|
readonly viewport = new Viewport();
|
|
readonly scroller = this.designer.createScroller(this.viewport);
|
|
|
|
mountViewport(viewport: Element | null) {
|
|
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?: BuiltinSimulatorRenderer;
|
|
get renderer() {
|
|
return this._renderer;
|
|
}
|
|
|
|
readonly componentsConsumer = new ResourceConsumer<Asset | undefined>(() => this.componentsAsset);
|
|
|
|
readonly injectionConsumer = new ResourceConsumer(() => {
|
|
return {};
|
|
});
|
|
|
|
readonly libraryMap: { [key: string]: string } = {};
|
|
|
|
private _iframe?: HTMLIFrameElement;
|
|
async mountContentFrame(iframe: HTMLIFrameElement | null) {
|
|
if (!iframe || this._iframe === iframe) {
|
|
return;
|
|
}
|
|
this._iframe = iframe;
|
|
|
|
this._contentWindow = iframe.contentWindow!;
|
|
|
|
const library = this.get('library') as LibraryItem[];
|
|
const libraryAsset: AssetList = [];
|
|
if (library) {
|
|
library.forEach((item) => {
|
|
this.libraryMap[item.package] = item.library;
|
|
libraryAsset.push(item.urls);
|
|
});
|
|
}
|
|
|
|
const vendors = [
|
|
// required & use once
|
|
assetBundle(this.get('environment') || defaultEnvironment, AssetLevel.Environment),
|
|
// required & use once
|
|
assetBundle(libraryAsset, AssetLevel.Library),
|
|
// required & TODO: think of update
|
|
assetBundle(this.theme, 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) => {
|
|
// stop response document focus event
|
|
downEvent.stopPropagation();
|
|
downEvent.preventDefault();
|
|
|
|
const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element);
|
|
const node = nodeInst?.node || this.document.rootNode;
|
|
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
|
|
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
|
|
const checkSelect = (e: MouseEvent) => {
|
|
doc.removeEventListener('mouseup', checkSelect, true);
|
|
if (!isShaken(downEvent, e)) {
|
|
const id = node.id;
|
|
designer.activeTracker.track(node);
|
|
if (isMulti && !isRootNode(node) && selection.has(id)) {
|
|
selection.remove(id);
|
|
} else {
|
|
selection.select(id);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isLeftButton && !isRootNode(node)) {
|
|
let nodes: Node[] = [node];
|
|
let ignoreUpSelected = false;
|
|
if (isMulti) {
|
|
// multi select mode, directily add
|
|
if (!selection.has(node.id)) {
|
|
designer.activeTracker.track(node);
|
|
selection.add(node.id);
|
|
ignoreUpSelected = true;
|
|
}
|
|
selection.remove(this.document.rootNode.id);
|
|
// 获得顶层 nodes
|
|
nodes = selection.getTopNodes();
|
|
} else if (selection.containsNode(node, true)) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
doc.addEventListener('mouseup', checkSelect, true);
|
|
},
|
|
true,
|
|
);
|
|
|
|
doc.addEventListener(
|
|
'click',
|
|
(e) => {
|
|
// stop response document click event
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
// todo: catch link redirect
|
|
},
|
|
true,
|
|
);
|
|
|
|
// cause edit
|
|
doc.addEventListener(
|
|
'dblclick',
|
|
(e: MouseEvent) => {
|
|
// stop response document dblclick event
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
// todo: quick editing
|
|
},
|
|
true,
|
|
);
|
|
}
|
|
|
|
private disableHovering?: () => void;
|
|
/**
|
|
* 设置悬停处理
|
|
*/
|
|
setupHovering() {
|
|
const doc = this.contentDocument!;
|
|
const hovering = this.document.designer.hovering;
|
|
const hover = (e: MouseEvent) => {
|
|
if (!hovering.enable) {
|
|
return;
|
|
}
|
|
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
|
|
hovering.hover(nodeInst?.node || null);
|
|
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;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
setSuspense(suspended: boolean) {
|
|
if (suspended) {
|
|
if (this.disableHovering) {
|
|
this.disableHovering();
|
|
}
|
|
// sleep some autorun reaction
|
|
} else {
|
|
// weekup some autorun reaction
|
|
if (!this.disableHovering) {
|
|
this.setupHovering();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
generateComponentMetadata(componentName: string): ComponentMetadata {
|
|
// if html tags
|
|
if (isHTMLTag(componentName)) {
|
|
return {
|
|
componentName,
|
|
// TODO: read builtins html metadata
|
|
};
|
|
}
|
|
|
|
const component = this.getComponent(componentName);
|
|
|
|
if (component) {
|
|
parseProps(component as any);
|
|
}
|
|
|
|
// TODO:
|
|
// 1. generate builtin div/p/h1/h2
|
|
// 2. read propTypes
|
|
return {
|
|
componentName,
|
|
props: parseProps(this.getComponent(componentName)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
getComponent(componentName: string): Component | null {
|
|
return this.renderer?.getComponent(componentName) || null;
|
|
}
|
|
|
|
@obx.val private instancesMap = new Map<string, ComponentInstance[]>();
|
|
setInstance(id: string, instances: ComponentInstance[] | null) {
|
|
if (instances == null) {
|
|
this.instancesMap.delete(id);
|
|
} else {
|
|
this.instancesMap.set(id, instances.slice());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
getComponentInstances(node: Node): ComponentInstance[] | null {
|
|
return this.instancesMap.get(node.id) || null;
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
getComponentInstanceId(instance: ComponentInstance) {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
getComponentContext(node: Node): object {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
getClosestNodeInstance(from: ComponentInstance, specId?: string): NodeInstance<ComponentInstance> | null {
|
|
return this.renderer?.getClosestNodeInstance(from, specId) || null;
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
computeRect(node: Node): Rect | null {
|
|
const instances = this.getComponentInstances(node);
|
|
if (!instances) {
|
|
return null;
|
|
}
|
|
return this.computeComponentInstanceRect(instances[0], node.componentMeta.rectSelector);
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): Rect | null {
|
|
const renderer = this.renderer!;
|
|
const elements = renderer.findDOMNodes(instance);
|
|
if (!elements) {
|
|
return null;
|
|
}
|
|
|
|
let elems = elements.slice();
|
|
if (selector) {
|
|
const matched = getMatched(elems, selector);
|
|
if (!matched) {
|
|
return null;
|
|
}
|
|
elems = [matched];
|
|
}
|
|
let rects: DOMRect[] | undefined;
|
|
let last: { x: number; y: number; r: number; b: number } | undefined;
|
|
let computed = false;
|
|
while (true) {
|
|
if (!rects || rects.length < 1) {
|
|
const elem = elems.pop();
|
|
if (!elem) {
|
|
break;
|
|
}
|
|
rects = renderer.getClientRects(elem);
|
|
}
|
|
const rect = rects.pop();
|
|
if (!rect) {
|
|
break;
|
|
}
|
|
if (rect.width === 0 && rect.height === 0) {
|
|
continue;
|
|
}
|
|
if (!last) {
|
|
last = {
|
|
x: rect.left,
|
|
y: rect.top,
|
|
r: rect.right,
|
|
b: rect.bottom,
|
|
};
|
|
continue;
|
|
}
|
|
if (rect.left < last.x) {
|
|
last.x = rect.left;
|
|
computed = true;
|
|
}
|
|
if (rect.top < last.y) {
|
|
last.y = rect.top;
|
|
computed = true;
|
|
}
|
|
if (rect.right > last.r) {
|
|
last.r = rect.right;
|
|
computed = true;
|
|
}
|
|
if (rect.bottom > last.b) {
|
|
last.b = rect.bottom;
|
|
computed = true;
|
|
}
|
|
}
|
|
|
|
if (last) {
|
|
const r: any = new DOMRect(last.x, last.y, last.r - last.x, last.b - last.y);
|
|
r.elements = elements;
|
|
r.computed = computed;
|
|
return r;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null {
|
|
return this._renderer?.findDOMNodes(instance) || null;
|
|
}
|
|
|
|
/**
|
|
* 通过 DOM 节点获取节点,依赖 simulator 的接口
|
|
*/
|
|
getNodeInstanceFromElement(target: Element | null): NodeInstance<ComponentInstance> | null {
|
|
if (!target) {
|
|
return null;
|
|
}
|
|
|
|
const nodeIntance = this.getClosestNodeInstance(target);
|
|
if (!nodeIntance) {
|
|
return null;
|
|
}
|
|
const node = this.document.getNode(nodeIntance.nodeId);
|
|
return {
|
|
...nodeIntance,
|
|
node,
|
|
};
|
|
}
|
|
|
|
private tryScrollAgain: number | null = null;
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
scrollToNode(node: Node, detail?: any, tryTimes = 0) {
|
|
this.tryScrollAgain = null;
|
|
if (this.sensing) {
|
|
// actived sensor
|
|
return;
|
|
}
|
|
|
|
const opt: any = {};
|
|
const 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);
|
|
}
|
|
}
|
|
|
|
// #region ========= drag and drop helpers =============
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
setNativeSelection(enableFlag: boolean) {
|
|
this.renderer?.setNativeSelection(enableFlag);
|
|
}
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
setDraggingState(state: boolean) {
|
|
this.renderer?.setDraggingState(state);
|
|
}
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
setCopyState(state: boolean) {
|
|
this.renderer?.setCopyState(state);
|
|
}
|
|
/**
|
|
* @see ISimulator
|
|
*/
|
|
clearState() {
|
|
this.renderer?.clearState();
|
|
}
|
|
|
|
private _sensorAvailable = true;
|
|
/**
|
|
* @see ISensor
|
|
*/
|
|
get sensorAvailable(): boolean {
|
|
return this._sensorAvailable;
|
|
}
|
|
|
|
/**
|
|
* @see ISensor
|
|
*/
|
|
fixEvent(e: LocateEvent): LocateEvent {
|
|
if (e.fixed) {
|
|
return e;
|
|
}
|
|
|
|
const notMyEvent = e.originalEvent.view?.document !== this.contentDocument;
|
|
// fix canvasX canvasY : 当前激活文档画布坐标系
|
|
if (notMyEvent || !('canvasX' in e) || !('canvasY' in e)) {
|
|
const l = this.viewport.toLocalPoint({
|
|
clientX: e.globalX,
|
|
clientY: e.globalY,
|
|
});
|
|
e.canvasX = l.clientX;
|
|
e.canvasY = l.clientY;
|
|
}
|
|
|
|
// fix target : 浏览器事件响应目标
|
|
if (!e.target || notMyEvent) {
|
|
e.target = this.contentDocument!.elementFromPoint(e.canvasX!, e.canvasY!);
|
|
}
|
|
|
|
// documentModel : 目标文档
|
|
e.documentModel = this.document;
|
|
|
|
// 事件已订正
|
|
e.fixed = true;
|
|
return e;
|
|
}
|
|
|
|
/**
|
|
* @see ISensor
|
|
*/
|
|
isEnter(e: LocateEvent): boolean {
|
|
const rect = this.viewport.bounds;
|
|
return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right;
|
|
}
|
|
|
|
private sensing = false;
|
|
/**
|
|
* @see ISensor
|
|
*/
|
|
deactiveSensor() {
|
|
this.sensing = false;
|
|
this.scroller.cancel();
|
|
}
|
|
|
|
// ========= drag location logic: hepler for locate ==========
|
|
|
|
/**
|
|
* @see ISensor
|
|
*/
|
|
locate(e: LocateEvent): any {
|
|
this.sensing = true;
|
|
this.scroller.scrolling(e);
|
|
const dropContainer = this.getDropContainer(e);
|
|
if (!dropContainer) {
|
|
return null;
|
|
}
|
|
|
|
if (isLocationData(dropContainer)) {
|
|
return this.designer.createLocation(dropContainer);
|
|
}
|
|
|
|
const { container, instance: containerInstance } = dropContainer;
|
|
|
|
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rectSelector);
|
|
|
|
if (!edge) {
|
|
return null;
|
|
}
|
|
|
|
const children = container.children;
|
|
|
|
const detail: LocationChildrenDetail = {
|
|
type: LocationDetailType.Children,
|
|
index: 0,
|
|
edge,
|
|
};
|
|
|
|
const locationData = {
|
|
target: container,
|
|
detail,
|
|
source: 'simulator' + this.document.id,
|
|
event: e,
|
|
};
|
|
|
|
if (!children || children.size < 1 || !edge) {
|
|
return this.designer.createLocation(locationData);
|
|
}
|
|
|
|
let nearRect = null;
|
|
let nearIndex = 0;
|
|
let nearNode = null;
|
|
let nearDistance = null;
|
|
let minTop = null;
|
|
let maxBottom = null;
|
|
|
|
for (let i = 0, l = children.size; i < l; i++) {
|
|
const node = children.get(i)!;
|
|
const index = i;
|
|
const instances = this.getComponentInstances(node);
|
|
const inst = instances
|
|
? instances.length > 1
|
|
? instances.find((inst) => this.getClosestNodeInstance(inst, container.id)?.instance === containerInstance)
|
|
: instances[0]
|
|
: null;
|
|
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null;
|
|
|
|
if (!rect) {
|
|
continue;
|
|
}
|
|
|
|
const distance = isPointInRect(e as any, rect) ? 0 : distanceToRect(e as any, rect);
|
|
|
|
if (distance === 0) {
|
|
nearDistance = distance;
|
|
nearNode = node;
|
|
nearIndex = index;
|
|
nearRect = rect;
|
|
break;
|
|
}
|
|
|
|
// 标记子节点最顶
|
|
if (minTop === null || rect.top < minTop) {
|
|
minTop = rect.top;
|
|
}
|
|
// 标记子节点最底
|
|
if (maxBottom === null || rect.bottom > maxBottom) {
|
|
maxBottom = 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;
|
|
|
|
console.info('mm', inline, vertical, row, el);
|
|
// TODO: fix type
|
|
const near: any = {
|
|
node: nearNode,
|
|
pos: 'before',
|
|
align: vertical ? 'V' : 'H',
|
|
};
|
|
detail.near = near;
|
|
if (isNearAfter(e as any, nearRect, vertical)) {
|
|
near.pos = 'after';
|
|
detail.index = nearIndex + 1;
|
|
}
|
|
if (!row && nearDistance !== 0) {
|
|
const edgeDistance = distanceToEdge(e as any, edge);
|
|
if (edgeDistance.distance < nearDistance!) {
|
|
const nearAfter = edgeDistance.nearAfter;
|
|
if (minTop == null) {
|
|
minTop = edge.top;
|
|
}
|
|
if (maxBottom == null) {
|
|
maxBottom = edge.bottom;
|
|
}
|
|
near.rect = new DOMRect(edge.left, minTop, edge.width, maxBottom - minTop);
|
|
near.align = 'H';
|
|
near.pos = nearAfter ? 'after' : 'before';
|
|
detail.index = nearAfter ? children.size : 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.designer.createLocation(locationData);
|
|
}
|
|
|
|
/**
|
|
* 查找合适的投放容器
|
|
*/
|
|
getDropContainer(e: LocateEvent): DropContainer | LocationData | null {
|
|
const { target, dragObject } = e;
|
|
const isAny = isDragAnyObject(dragObject);
|
|
const { modalNode, currentRoot } = this.document;
|
|
let container: Node;
|
|
let nodeInstance: NodeInstance<ComponentInstance> | undefined;
|
|
|
|
if (target) {
|
|
const ref = this.getNodeInstanceFromElement(target);
|
|
if (ref?.node) {
|
|
nodeInstance = ref;
|
|
container = ref.node;
|
|
} else if (isAny) {
|
|
return null;
|
|
} else {
|
|
container = currentRoot;
|
|
}
|
|
} else if (isAny) {
|
|
return null;
|
|
} else {
|
|
container = currentRoot;
|
|
}
|
|
|
|
if (!isNodeParent(container)) {
|
|
container = container.parent || currentRoot;
|
|
}
|
|
|
|
// check container if in modalNode layer, if not, use modalNode
|
|
if (modalNode && !modalNode.contains(container)) {
|
|
container = modalNode;
|
|
}
|
|
|
|
// TODO: use spec container to accept specialData
|
|
if (isAny) {
|
|
// will return locationData
|
|
return null;
|
|
}
|
|
|
|
// get common parent, avoid drop container contains by dragObject
|
|
// TODO: renderengine support pointerEvents: none for acceleration
|
|
const drillDownExcludes = new Set<Node>();
|
|
if (isDragNodeObject(dragObject)) {
|
|
const nodes = dragObject.nodes;
|
|
let i = nodes.length;
|
|
let p: any = container;
|
|
while (i-- > 0) {
|
|
if (contains(nodes[i], p)) {
|
|
p = nodes[i].parent;
|
|
}
|
|
}
|
|
if (p !== container) {
|
|
container = p || this.document.rootNode;
|
|
drillDownExcludes.add(container);
|
|
}
|
|
}
|
|
|
|
const ret: any = {
|
|
container,
|
|
};
|
|
if (nodeInstance) {
|
|
if (nodeInstance.node === container) {
|
|
ret.instance = nodeInstance.instance;
|
|
} else {
|
|
ret.instance = this.getClosestNodeInstance(nodeInstance.instance as any, container.id)?.instance;
|
|
}
|
|
} else {
|
|
ret.instance = this.getComponentInstances(container)?.[0];
|
|
}
|
|
|
|
let res: any;
|
|
let upward: any;
|
|
// TODO: complete drill down logic
|
|
while (container) {
|
|
if (ret.container !== container) {
|
|
ret.container = container;
|
|
ret.instance = this.getClosestNodeInstance(ret.instance, container.id)?.instance;
|
|
}
|
|
res = this.handleAccept(ret, e);
|
|
if (isLocationData(res)) {
|
|
return res;
|
|
}
|
|
if (res === true) {
|
|
return ret;
|
|
}
|
|
if (!res) {
|
|
drillDownExcludes.add(container);
|
|
if (upward) {
|
|
container = upward;
|
|
upward = null;
|
|
} else if (container.parent) {
|
|
container = container.parent;
|
|
} else {
|
|
return null;
|
|
}
|
|
} else if (isNode(res)) {
|
|
/* else if (res === DRILL_DOWN) {
|
|
if (!upward) {
|
|
upward = container.parent;
|
|
}
|
|
container = this.getNearByContainer(container, drillExcludes, e);
|
|
if (!container) {
|
|
container = upward;
|
|
upward = null;
|
|
}
|
|
}*/
|
|
container = res;
|
|
upward = null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
isAcceptable(container: NodeParent): boolean {
|
|
return false;
|
|
/*
|
|
const meta = container.componentMeta;
|
|
const instance: any = this.document.getView(container);
|
|
if (instance && '$accept' in instance) {
|
|
return true;
|
|
}
|
|
return meta.acceptable;
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* 控制接受
|
|
*/
|
|
handleAccept({ container, instance }: DropContainer, e: LocateEvent) {
|
|
const { dragObject } = e;
|
|
if (isRootNode(container)) {
|
|
return this.document.checkDropTarget(container, dragObject as any);
|
|
}
|
|
|
|
const meta = container.componentMeta;
|
|
|
|
// FIXME: get containerInstance for accept logic use
|
|
const acceptable: boolean = this.isAcceptable(container);
|
|
if (!meta.isContainer && !acceptable) {
|
|
return false;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
// check nesting
|
|
return this.document.checkNesting(container, dragObject 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;
|
|
*/
|
|
}
|
|
// #endregion
|
|
}
|
|
|
|
function isHTMLTag(name: string) {
|
|
return /^[a-z]\w*$/.test(name);
|
|
}
|
|
|
|
function isPointInRect(point: CanvasPoint, rect: Rect) {
|
|
return (
|
|
point.canvasY >= rect.top &&
|
|
point.canvasY <= rect.bottom &&
|
|
point.canvasX >= rect.left &&
|
|
point.canvasX <= rect.right
|
|
);
|
|
}
|
|
|
|
function distanceToRect(point: CanvasPoint, rect: Rect) {
|
|
let minX = Math.min(Math.abs(point.canvasX - rect.left), Math.abs(point.canvasX - rect.right));
|
|
let minY = Math.min(Math.abs(point.canvasY - rect.top), Math.abs(point.canvasY - rect.bottom));
|
|
if (point.canvasX >= rect.left && point.canvasX <= rect.right) {
|
|
minX = 0;
|
|
}
|
|
if (point.canvasY >= rect.top && point.canvasY <= rect.bottom) {
|
|
minY = 0;
|
|
}
|
|
|
|
return Math.sqrt(minX ** 2 + minY ** 2);
|
|
}
|
|
|
|
function distanceToEdge(point: CanvasPoint, rect: Rect) {
|
|
const distanceTop = Math.abs(point.canvasY - rect.top);
|
|
const distanceBottom = Math.abs(point.canvasY - rect.bottom);
|
|
|
|
return {
|
|
distance: Math.min(distanceTop, distanceBottom),
|
|
nearAfter: distanceBottom < distanceTop,
|
|
};
|
|
}
|
|
|
|
function isNearAfter(point: CanvasPoint, rect: Rect, inline: boolean) {
|
|
if (inline) {
|
|
return (
|
|
Math.abs(point.canvasX - rect.left) + Math.abs(point.canvasY - rect.top) >
|
|
Math.abs(point.canvasX - rect.right) + Math.abs(point.canvasY - rect.bottom)
|
|
);
|
|
}
|
|
return Math.abs(point.canvasY - rect.top) > Math.abs(point.canvasY - rect.bottom);
|
|
}
|
|
|
|
function getMatched(elements: Array<Element | Text>, selector: string): Element | null {
|
|
let firstQueried: Element | null = null;
|
|
for (const elem of elements) {
|
|
if (isElement(elem)) {
|
|
if (elem.matches(selector)) {
|
|
return elem;
|
|
}
|
|
|
|
if (!firstQueried) {
|
|
firstQueried = elem.querySelector(selector);
|
|
}
|
|
}
|
|
}
|
|
return firstQueried;
|
|
}
|
|
|
|
interface DropContainer {
|
|
container: NodeParent;
|
|
instance: ComponentInstance;
|
|
}
|