mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-11 18:42:56 +00:00
1685 lines
47 KiB
TypeScript
1685 lines
47 KiB
TypeScript
import {
|
||
obx,
|
||
autorun,
|
||
reaction,
|
||
computed,
|
||
getPublicPath,
|
||
hotkey,
|
||
focusTracker,
|
||
engineConfig,
|
||
IReactionPublic,
|
||
IReactionOptions,
|
||
IReactionDisposer,
|
||
makeObservable,
|
||
} from '@alilc/lowcode-editor-core';
|
||
import { EventEmitter } from 'events';
|
||
import {
|
||
ISimulatorHost,
|
||
Component,
|
||
NodeInstance,
|
||
ComponentInstance,
|
||
DropContainer,
|
||
} from '../simulator';
|
||
import Viewport from './viewport';
|
||
import { createSimulator } from './create-simulator';
|
||
import { Node, ParentalNode, contains, isRootNode, isLowCodeComponent } from '../document';
|
||
import ResourceConsumer from './resource-consumer';
|
||
import {
|
||
AssetLevel,
|
||
Asset,
|
||
AssetList,
|
||
assetBundle,
|
||
assetItem,
|
||
AssetType,
|
||
isElement,
|
||
isFormEvent,
|
||
hasOwnProperty,
|
||
UtilsMetadata,
|
||
getClosestNode,
|
||
transactionManager,
|
||
} from '@alilc/lowcode-utils';
|
||
import {
|
||
isShaken,
|
||
LocateEvent,
|
||
isDragAnyObject,
|
||
isDragNodeObject,
|
||
isLocationData,
|
||
LocationChildrenDetail,
|
||
LocationDetailType,
|
||
isChildInline,
|
||
isRowContainer,
|
||
getRectTarget,
|
||
Rect,
|
||
CanvasPoint,
|
||
Designer,
|
||
} from '../designer';
|
||
import { parseMetadata } from './utils/parse-metadata';
|
||
import { getClosestClickableNode } from './utils/clickable';
|
||
import {
|
||
ComponentMetadata,
|
||
ComponentSchema,
|
||
Package,
|
||
TransitionType,
|
||
DragObjectType,
|
||
DragNodeObject,
|
||
} from '@alilc/lowcode-types';
|
||
import { BuiltinSimulatorRenderer } from './renderer';
|
||
import clipboard from '../designer/clipboard';
|
||
import { LiveEditing } from './live-editing/live-editing';
|
||
import { Project } from '../project';
|
||
import { Scroller } from '../designer/scroller';
|
||
import { isElementNode, isDOMNodeVisible } from '../utils/misc';
|
||
import { debounce } from 'lodash';
|
||
|
||
export interface LibraryItem extends Package{
|
||
package: string;
|
||
library: string;
|
||
urls?: Asset;
|
||
editUrls?: Asset;
|
||
}
|
||
|
||
export interface DeviceStyleProps {
|
||
canvas?: object;
|
||
viewport?: object;
|
||
}
|
||
|
||
export interface BuiltinSimulatorProps {
|
||
// 从 documentModel 上获取
|
||
// suspended?: boolean;
|
||
designMode?: 'live' | 'design' | 'preview' | 'extend' | 'border';
|
||
device?: 'mobile' | 'iphone' | string;
|
||
deviceClassName?: string;
|
||
environment?: Asset;
|
||
// @TODO 补充类型
|
||
/** @property 请求处理器配置 */
|
||
requestHandlersMap?: any;
|
||
extraEnvironment?: Asset;
|
||
library?: LibraryItem[];
|
||
utilsMetadata?: UtilsMetadata;
|
||
simulatorUrl?: Asset;
|
||
theme?: Asset;
|
||
componentsAsset?: Asset;
|
||
[key: string]: any;
|
||
}
|
||
|
||
const defaultSimulatorUrl = (() => {
|
||
const publicPath = getPublicPath();
|
||
let urls;
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
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.css`, `${prefix}/react-simulator-renderer.js`];
|
||
} else {
|
||
urls = [`${prefix}/react-simulator-renderer.css`, `${prefix}/react-simulator-renderer.js`];
|
||
}
|
||
return urls;
|
||
})();
|
||
|
||
const defaultRaxSimulatorUrl = (() => {
|
||
const publicPath = getPublicPath();
|
||
let urls;
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||
const [_, prefix = '', dev] = /^(.+?)(\/js)?\/?$/.exec(publicPath) || [];
|
||
if (dev) {
|
||
urls = [`${prefix}/css/rax-simulator-renderer.css`, `${prefix}/js/rax-simulator-renderer.js`];
|
||
} else if (process.env.NODE_ENV === 'production') {
|
||
urls = [`${prefix}/rax-simulator-renderer.css`, `${prefix}/rax-simulator-renderer.js`];
|
||
} else {
|
||
urls = [`${prefix}/rax-simulator-renderer.css`, `${prefix}/rax-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;window.__is_simulator_env__=true;',
|
||
undefined,
|
||
'react',
|
||
),
|
||
assetItem(
|
||
AssetType.JSText,
|
||
'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;',
|
||
),
|
||
];
|
||
|
||
const defaultRaxEnvironment = [
|
||
assetItem(
|
||
AssetType.JSText,
|
||
'window.Rax=parent.Rax;window.React=parent.React;window.ReactDOM=parent.ReactDOM;window.VisualEngineUtils=parent.VisualEngineUtils;window.VisualEngine=parent.VisualEngine',
|
||
),
|
||
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;
|
||
|
||
readonly project: Project;
|
||
|
||
readonly designer: Designer;
|
||
|
||
readonly viewport = new Viewport();
|
||
|
||
readonly scroller: Scroller;
|
||
|
||
readonly emitter: EventEmitter = new EventEmitter();
|
||
|
||
readonly componentsConsumer: ResourceConsumer;
|
||
|
||
readonly injectionConsumer: ResourceConsumer;
|
||
|
||
readonly i18nConsumer: ResourceConsumer;
|
||
|
||
/**
|
||
* 是否为画布自动渲染
|
||
*/
|
||
autoRender = true;
|
||
|
||
stopAutoRepaintNode() {
|
||
this.renderer?.stopAutoRepaintNode();
|
||
}
|
||
|
||
enableAutoRepaintNode() {
|
||
this.renderer?.enableAutoRepaintNode();
|
||
}
|
||
|
||
constructor(project: Project) {
|
||
makeObservable(this);
|
||
this.project = project;
|
||
this.designer = project?.designer;
|
||
this.scroller = this.designer.createScroller(this.viewport);
|
||
this.autoRender = !engineConfig.get('disableAutoRender', false);
|
||
this.componentsConsumer = new ResourceConsumer<Asset | undefined>(() => this.componentsAsset);
|
||
this.injectionConsumer = new ResourceConsumer(() => {
|
||
return {
|
||
appHelper: engineConfig.get('appHelper'),
|
||
};
|
||
});
|
||
|
||
this.i18nConsumer = new ResourceConsumer(() => this.project.i18n);
|
||
|
||
transactionManager.onStartTransaction(() => {
|
||
this.stopAutoRepaintNode();
|
||
}, TransitionType.REPAINT);
|
||
// 防止批量调用 transaction 时,执行多次 rerender
|
||
const rerender = debounce(this.rerender.bind(this), 28);
|
||
transactionManager.onEndTransaction(() => {
|
||
rerender();
|
||
this.enableAutoRepaintNode();
|
||
}, TransitionType.REPAINT);
|
||
}
|
||
|
||
get currentDocument() {
|
||
return this.project.currentDocument;
|
||
}
|
||
|
||
@computed get renderEnv(): string {
|
||
return this.get('renderEnv') || 'default';
|
||
}
|
||
|
||
@computed get device(): string {
|
||
return this.get('device') || 'default';
|
||
}
|
||
|
||
@computed get locale(): string {
|
||
return this.get('locale');
|
||
}
|
||
|
||
@computed get deviceClassName(): string | undefined {
|
||
return this.get('deviceClassName');
|
||
}
|
||
|
||
@computed get designMode(): 'live' | 'design' | 'preview' {
|
||
// renderer 依赖
|
||
// TODO: 需要根据 design mode 不同切换鼠标响应情况
|
||
return this.get('designMode') || 'design';
|
||
}
|
||
|
||
@computed get requestHandlersMap(): any {
|
||
// renderer 依赖
|
||
// TODO: 需要根据 design mode 不同切换鼠标响应情况
|
||
return this.get('requestHandlersMap') || null;
|
||
}
|
||
|
||
get thisRequiredInJSE(): any {
|
||
return engineConfig.get('thisRequiredInJSE') ?? true;
|
||
}
|
||
|
||
get enableStrictNotFoundMode(): any {
|
||
return engineConfig.get('enableStrictNotFoundMode') ?? false;
|
||
}
|
||
|
||
get notFoundComponent(): any {
|
||
return engineConfig.get('notFoundComponent') ?? null;
|
||
}
|
||
|
||
get faultComponent(): any {
|
||
return engineConfig.get('faultComponent') ?? null;
|
||
}
|
||
|
||
@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;
|
||
}
|
||
|
||
@computed get deviceStyle(): DeviceStyleProps | undefined {
|
||
return this.get('deviceStyle');
|
||
}
|
||
|
||
@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 {
|
||
if (key === 'device') {
|
||
return (
|
||
this.designer?.editor?.get('deviceMapper')?.transform?.(this._props.device) ||
|
||
this._props.device
|
||
);
|
||
}
|
||
return this._props[key];
|
||
}
|
||
|
||
/**
|
||
* 有 Renderer 进程连接进来,设置同步机制
|
||
*/
|
||
connect(
|
||
renderer: BuiltinSimulatorRenderer,
|
||
effect: (reaction: IReactionPublic) => void, options?: IReactionOptions,
|
||
) {
|
||
this._renderer = renderer;
|
||
return autorun(effect, options);
|
||
}
|
||
|
||
reaction(expression: (reaction: IReactionPublic) => unknown, effect: (value: unknown, prev: unknown, reaction: IReactionPublic) => void,
|
||
opts?: IReactionOptions | undefined): IReactionDisposer {
|
||
return reaction(expression, effect, opts);
|
||
}
|
||
|
||
autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions): IReactionDisposer {
|
||
return autorun(effect, options);
|
||
}
|
||
|
||
purge(): void {
|
||
// todo
|
||
}
|
||
|
||
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 asyncLibraryMap: { [key: string]: {} } = {};
|
||
|
||
readonly libraryMap: { [key: string]: string } = {};
|
||
|
||
private _iframe?: HTMLIFrameElement;
|
||
|
||
/**
|
||
* {
|
||
* "title":"BizCharts",
|
||
* "package":"bizcharts",
|
||
* "exportName":"bizcharts",
|
||
* "version":"4.0.14",
|
||
* "urls":[
|
||
* "https://g.alicdn.com/code/lib/bizcharts/4.0.14/BizCharts.js"
|
||
* ],
|
||
* "library":"BizCharts"
|
||
* }
|
||
* package:String 资源npm包名
|
||
* exportName:String umd包导出名字,用于适配部分物料包define name不一致的问题,例如把BizCharts改成bizcharts,用来兼容物料用define声明的bizcharts
|
||
* version:String 版本号
|
||
* urls:Array 资源cdn地址,必须是umd类型,可以是.js或者.css
|
||
* library:String umd包直接导出的name
|
||
*/
|
||
buildLibrary(library?: LibraryItem[]) {
|
||
const _library = library || (this.get('library') as LibraryItem[]);
|
||
const libraryAsset: AssetList = [];
|
||
const libraryExportList: string[] = [];
|
||
const functionCallLibraryExportList: string[] = [];
|
||
|
||
if (_library && _library.length) {
|
||
_library.forEach((item) => {
|
||
const { exportMode, exportSourceLibrary } = item;
|
||
this.libraryMap[item.package] = item.library;
|
||
if (item.async) {
|
||
this.asyncLibraryMap[item.package] = item;
|
||
}
|
||
if (item.exportName && item.library) {
|
||
libraryExportList.push(
|
||
`Object.defineProperty(window,'${item.exportName}',{get:()=>window.${item.library}});`,
|
||
);
|
||
}
|
||
if (exportMode === 'functionCall' && exportSourceLibrary) {
|
||
functionCallLibraryExportList.push(
|
||
`window["${item.library}"] = window["${exportSourceLibrary}"]("${item.library}", "${item.package}");`,
|
||
);
|
||
}
|
||
if (item.editUrls) {
|
||
libraryAsset.push(item.editUrls);
|
||
} else if (item.urls) {
|
||
libraryAsset.push(item.urls);
|
||
}
|
||
});
|
||
}
|
||
libraryAsset.unshift(assetItem(AssetType.JSText, libraryExportList.join('')));
|
||
libraryAsset.push(assetItem(AssetType.JSText, functionCallLibraryExportList.join('')));
|
||
return libraryAsset;
|
||
}
|
||
|
||
rerender() {
|
||
this.designer.refreshComponentMetasMap();
|
||
this.renderer?.rerender?.();
|
||
}
|
||
|
||
async mountContentFrame(iframe: HTMLIFrameElement | null) {
|
||
if (!iframe || this._iframe === iframe) {
|
||
return;
|
||
}
|
||
this._iframe = iframe;
|
||
|
||
this._contentWindow = iframe.contentWindow!;
|
||
this._contentDocument = this._contentWindow.document;
|
||
|
||
const libraryAsset: AssetList = this.buildLibrary();
|
||
|
||
const vendors = [
|
||
// required & use once
|
||
assetBundle(
|
||
this.get('environment') ||
|
||
(this.renderEnv === 'rax' ? defaultRaxEnvironment : defaultEnvironment),
|
||
AssetLevel.Environment,
|
||
),
|
||
// required & use once
|
||
assetBundle(this.get('extraEnvironment'), 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') ||
|
||
(this.renderEnv === 'rax' ? defaultRaxSimulatorUrl : defaultSimulatorUrl),
|
||
AssetLevel.Runtime,
|
||
),
|
||
];
|
||
|
||
// wait 准备 iframe 内容、依赖库注入
|
||
const renderer = await createSimulator(this, iframe, vendors);
|
||
|
||
// TODO: !!! thinkof reload onloa
|
||
|
||
// wait 业务组件被第一次消费,否则会渲染出错
|
||
await this.componentsConsumer.waitFirstConsume();
|
||
|
||
// wait 运行时上下文
|
||
await this.injectionConsumer.waitFirstConsume();
|
||
|
||
if (Object.keys(this.asyncLibraryMap).length > 0) {
|
||
// 加载异步Library
|
||
await renderer.loadAsyncLibrary(this.asyncLibraryMap);
|
||
Object.keys(this.asyncLibraryMap).forEach(key => {
|
||
delete this.asyncLibraryMap[key];
|
||
});
|
||
}
|
||
|
||
// step 5 ready & render
|
||
renderer.run();
|
||
|
||
// init events, overlays
|
||
this.viewport.setScrollTarget(this._contentWindow);
|
||
this.setupEvents();
|
||
|
||
// bind hotkey & clipboard
|
||
hotkey.mount(this._contentWindow);
|
||
focusTracker.mount(this._contentWindow);
|
||
clipboard.injectCopyPaster(this._contentDocument);
|
||
|
||
// TODO: dispose the bindings
|
||
}
|
||
|
||
async setupComponents(library) {
|
||
const libraryAsset: AssetList = this.buildLibrary(library);
|
||
await this.renderer?.load(libraryAsset);
|
||
if (Object.keys(this.asyncLibraryMap).length > 0) {
|
||
// 加载异步Library
|
||
await this.renderer?.loadAsyncLibrary(this.asyncLibraryMap);
|
||
Object.keys(this.asyncLibraryMap).forEach(key => {
|
||
delete this.asyncLibraryMap[key];
|
||
});
|
||
}
|
||
}
|
||
|
||
setupEvents() {
|
||
// TODO: Thinkof move events control to simulator renderer
|
||
// just listen special callback
|
||
// because iframe maybe reload
|
||
this.setupDragAndClick();
|
||
this.setupDetecting();
|
||
this.setupLiveEditing();
|
||
this.setupContextMenu();
|
||
}
|
||
|
||
postEvent(eventName: string, ...data: any[]) {
|
||
this.emitter.emit(eventName, ...data);
|
||
}
|
||
|
||
setupDragAndClick() {
|
||
const { designer } = this;
|
||
const doc = this.contentDocument!;
|
||
|
||
// TODO: think of lock when edit a node
|
||
// 事件路由
|
||
doc.addEventListener(
|
||
'mousedown',
|
||
(downEvent: MouseEvent) => {
|
||
// fix for popups close logic
|
||
document.dispatchEvent(new Event('mousedown'));
|
||
const documentModel = this.project.currentDocument;
|
||
if (this.liveEditing.editing || !documentModel) {
|
||
return;
|
||
}
|
||
const { selection } = documentModel;
|
||
let isMulti = false;
|
||
if (this.designMode === 'design') {
|
||
isMulti = downEvent.metaKey || downEvent.ctrlKey;
|
||
} else if (!downEvent.metaKey) {
|
||
return;
|
||
}
|
||
// FIXME: dirty fix remove label-for fro liveEditing
|
||
(downEvent.target as HTMLElement).removeAttribute('for');
|
||
const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element);
|
||
const focusNode = documentModel.focusNode;
|
||
const node = getClosestClickableNode(nodeInst?.node || focusNode, downEvent);
|
||
// 如果找不到可点击的节点, 直接返回
|
||
if (!node) {
|
||
return;
|
||
}
|
||
// 触发 onMouseDownHook 钩子
|
||
const onMouseDownHook = node.componentMeta?.getMetadata()?.configure.advanced?.callbacks?.onMouseDownHook;
|
||
if (onMouseDownHook) {
|
||
onMouseDownHook(downEvent, node.internalToShellNode());
|
||
}
|
||
const rglNode = node?.getParent();
|
||
const isRGLNode = rglNode?.isRGLContainer;
|
||
if (isRGLNode) {
|
||
// 如果拖拽的是磁铁块的右下角handle,则直接跳过
|
||
if (downEvent.target.classList.contains('react-resizable-handle')) return;
|
||
// 禁止多选
|
||
isMulti = false;
|
||
designer.dragon.emitter.emit('rgl.switch', {
|
||
action: 'start',
|
||
rglNode,
|
||
});
|
||
} else {
|
||
// stop response document focus event
|
||
// 禁止原生拖拽
|
||
downEvent.stopPropagation();
|
||
downEvent.preventDefault();
|
||
}
|
||
// if (!node?.isValidComponent()) {
|
||
// // 对于未注册组件直接返回
|
||
// return;
|
||
// }
|
||
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
|
||
const checkSelect = (e: MouseEvent) => {
|
||
doc.removeEventListener('mouseup', checkSelect, true);
|
||
// 取消移动;
|
||
designer.dragon.emitter.emit('rgl.switch', {
|
||
action: 'end',
|
||
rglNode,
|
||
});
|
||
// 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件,偶尔点击不能选中,磁帖块移除shaken检测
|
||
if (!isShaken(downEvent, e) || isRGLNode) {
|
||
let { id } = node;
|
||
designer.activeTracker.track({ node, instance: nodeInst?.instance });
|
||
if (isMulti && !node.contains(focusNode) && selection.has(id)) {
|
||
selection.remove(id);
|
||
} else {
|
||
// TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式
|
||
if (node.isPage() && node.getChildren()?.notEmpty() && this.designMode === 'live') {
|
||
const firstChildId = node.getChildren()?.get(0)?.getId();
|
||
if (firstChildId) id = firstChildId;
|
||
}
|
||
selection.select(node.contains(focusNode) ? focusNode.id : id);
|
||
|
||
// dirty code should refector
|
||
const editor = this.designer?.editor;
|
||
const npm = node?.componentMeta?.npm;
|
||
const selected =
|
||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
||
node?.componentMeta?.componentName ||
|
||
'';
|
||
editor?.emit('designer.builtinSimulator.select', {
|
||
selected,
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
if (isLeftButton && !node.contains(focusNode)) {
|
||
let nodes: Node[] = [node];
|
||
let ignoreUpSelected = false;
|
||
if (isMulti) {
|
||
// multi select mode, directily add
|
||
if (!selection.has(node.id)) {
|
||
designer.activeTracker.track({ node, instance: nodeInst?.instance });
|
||
selection.add(node.id);
|
||
ignoreUpSelected = true;
|
||
}
|
||
selection.remove(focusNode.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,
|
||
isRGLNode ? rglNode : undefined,
|
||
);
|
||
if (ignoreUpSelected) {
|
||
// multi select mode has add selected, should return
|
||
return;
|
||
}
|
||
}
|
||
|
||
doc.addEventListener('mouseup', checkSelect, true);
|
||
},
|
||
true,
|
||
);
|
||
|
||
doc.addEventListener(
|
||
'click',
|
||
(e) => {
|
||
// fix for popups close logic
|
||
const x = new Event('click');
|
||
x.initEvent('click', true);
|
||
this._iframe?.dispatchEvent(x);
|
||
const target = e.target as HTMLElement;
|
||
|
||
const customizeIgnoreSelectors = engineConfig.get('customizeIgnoreSelectors');
|
||
// TODO: need more elegant solution to ignore click events of components in designer
|
||
const defaultIgnoreSelectors: any = [
|
||
'.next-input-group',
|
||
'.next-checkbox-group',
|
||
'.next-checkbox-wrapper',
|
||
'.next-date-picker',
|
||
'.next-input',
|
||
'.next-month-picker',
|
||
'.next-number-picker',
|
||
'.next-radio-group',
|
||
'.next-range',
|
||
'.next-range-picker',
|
||
'.next-rating',
|
||
'.next-select',
|
||
'.next-switch',
|
||
'.next-time-picker',
|
||
'.next-upload',
|
||
'.next-year-picker',
|
||
'.next-breadcrumb-item',
|
||
'.next-calendar-header',
|
||
'.next-calendar-table',
|
||
'.editor-container', // 富文本组件
|
||
];
|
||
const ignoreSelectors = customizeIgnoreSelectors?.(defaultIgnoreSelectors, e) || defaultIgnoreSelectors;
|
||
const ignoreSelectorsString = ignoreSelectors.join(',');
|
||
// 提供了 customizeIgnoreSelectors 的情况下,忽略 isFormEvent() 判断
|
||
if ((!customizeIgnoreSelectors && isFormEvent(e)) || target?.closest(ignoreSelectorsString)) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
// stop response document click event
|
||
// todo: catch link redirect
|
||
},
|
||
true,
|
||
);
|
||
}
|
||
|
||
private disableHovering?: () => void;
|
||
|
||
private disableDetecting?: () => void;
|
||
|
||
/**
|
||
* 设置悬停处理
|
||
*/
|
||
setupDetecting() {
|
||
const doc = this.contentDocument!;
|
||
const { detecting, dragon } = this.designer;
|
||
const hover = (e: MouseEvent) => {
|
||
if (!detecting.enable || this.designMode !== 'design') {
|
||
return;
|
||
}
|
||
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
|
||
if (nodeInst?.node) {
|
||
let node = nodeInst.node;
|
||
const focusNode = node.document?.focusNode;
|
||
if (node.contains(focusNode)) {
|
||
node = focusNode;
|
||
}
|
||
detecting.capture(node);
|
||
} else {
|
||
detecting.capture(null);
|
||
}
|
||
if (!engineConfig.get('enableMouseEventPropagationInCanvas', false) || dragon.dragging) {
|
||
e.stopPropagation();
|
||
}
|
||
};
|
||
const leave = () => detecting.leave(this.project.currentDocument);
|
||
|
||
doc.addEventListener('mouseover', hover, true);
|
||
doc.addEventListener('mouseleave', leave, false);
|
||
|
||
// TODO: refactor this line, contains click, mousedown, mousemove
|
||
doc.addEventListener(
|
||
'mousemove',
|
||
(e: Event) => {
|
||
if (!engineConfig.get('enableMouseEventPropagationInCanvas', false) || dragon.dragging) {
|
||
e.stopPropagation();
|
||
}
|
||
},
|
||
true,
|
||
);
|
||
|
||
// this.disableDetecting = () => {
|
||
// detecting.leave(this.project.currentDocument);
|
||
// doc.removeEventListener('mouseover', hover, true);
|
||
// doc.removeEventListener('mouseleave', leave, false);
|
||
// this.disableDetecting = undefined;
|
||
// };
|
||
}
|
||
|
||
readonly liveEditing = new LiveEditing();
|
||
|
||
setupLiveEditing() {
|
||
const doc = this.contentDocument!;
|
||
// cause edit
|
||
doc.addEventListener(
|
||
'dblclick',
|
||
(e: MouseEvent) => {
|
||
// stop response document dblclick event
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
|
||
const targetElement = e.target as HTMLElement;
|
||
const nodeInst = this.getNodeInstanceFromElement(targetElement);
|
||
if (!nodeInst) {
|
||
return;
|
||
}
|
||
const focusNode = this.project.currentDocument?.focusNode;
|
||
const node = nodeInst.node || focusNode;
|
||
if (!node || isLowCodeComponent(node)) {
|
||
return;
|
||
}
|
||
|
||
const rootElement = this.findDOMNodes(
|
||
nodeInst.instance,
|
||
node.componentMeta.rootSelector,
|
||
)?.find(
|
||
(item) =>
|
||
// 可能是 [null];
|
||
item && item.contains(targetElement),
|
||
) as HTMLElement;
|
||
if (!rootElement) {
|
||
return;
|
||
}
|
||
|
||
this.liveEditing.apply({
|
||
node,
|
||
rootElement,
|
||
event: e,
|
||
});
|
||
},
|
||
true,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @see ISimulator
|
||
*/
|
||
setSuspense(suspended: boolean) {
|
||
return false;
|
||
// if (suspended) {
|
||
// /*
|
||
// if (this.disableDetecting) {
|
||
// this.disableDetecting();
|
||
// }
|
||
// */
|
||
// // sleep some autorun reaction
|
||
// } else {
|
||
// // weekup some autorun reaction
|
||
// /*
|
||
// if (!this.disableDetecting) {
|
||
// this.setupDetecting();
|
||
// }
|
||
// */
|
||
// }
|
||
}
|
||
|
||
setupContextMenu() {
|
||
const doc = this.contentDocument!;
|
||
doc.addEventListener('contextmenu', (e: MouseEvent) => {
|
||
const targetElement = e.target as HTMLElement;
|
||
const nodeInst = this.getNodeInstanceFromElement(targetElement);
|
||
if (!nodeInst) {
|
||
return;
|
||
}
|
||
const node = nodeInst.node || this.project.currentDocument?.focusNode;
|
||
if (!node) {
|
||
return;
|
||
}
|
||
|
||
// dirty code should refector
|
||
const editor = this.designer?.editor;
|
||
const npm = node?.componentMeta?.npm;
|
||
const selected =
|
||
[npm?.package, npm?.componentName].filter((item) => !!item).join('-') ||
|
||
node?.componentMeta?.componentName ||
|
||
'';
|
||
editor?.emit('designer.builtinSimulator.contextmenu', {
|
||
selected,
|
||
...nodeInst,
|
||
instanceRect: this.computeComponentInstanceRect(nodeInst.instance),
|
||
originalEvent: e,
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @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) {
|
||
return {
|
||
componentName,
|
||
};
|
||
}
|
||
|
||
// TODO:
|
||
// 1. generate builtin div/p/h1/h2
|
||
// 2. read propTypes
|
||
|
||
return {
|
||
componentName,
|
||
...parseMetadata(component),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @see ISimulator
|
||
*/
|
||
getComponent(componentName: string): Component | null {
|
||
return this.renderer?.getComponent(componentName) || null;
|
||
}
|
||
|
||
createComponent(schema: ComponentSchema): Component | null {
|
||
return null;
|
||
// return this.renderer?.createComponent(schema) || null;
|
||
}
|
||
|
||
@obx private instancesMap: {
|
||
[docId: string]: Map<string, ComponentInstance[]>;
|
||
} = {};
|
||
setInstance(docId: string, id: string, instances: ComponentInstance[] | null) {
|
||
if (!hasOwnProperty(this.instancesMap, docId)) {
|
||
this.instancesMap[docId] = new Map();
|
||
}
|
||
if (instances == null) {
|
||
this.instancesMap[docId].delete(id);
|
||
} else {
|
||
this.instancesMap[docId].set(id, instances.slice());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @see ISimulator
|
||
*/
|
||
getComponentInstances(node: Node, context?: NodeInstance): ComponentInstance[] | null {
|
||
const docId = node.document.id;
|
||
|
||
const instances = this.instancesMap[docId]?.get(node.id) || null;
|
||
if (!instances || !context) {
|
||
return instances;
|
||
}
|
||
|
||
// filter with context
|
||
return instances.filter((instance) => {
|
||
return this.getClosestNodeInstance(instance, context?.nodeId)?.instance === context.instance;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @see ISimulator
|
||
*/
|
||
getComponentContext(/* node: Node */): any {
|
||
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.rootSelector);
|
||
}
|
||
|
||
/**
|
||
* @see ISimulator
|
||
*/
|
||
computeComponentInstanceRect(instance: ComponentInstance, selector?: string): Rect | null {
|
||
const renderer = this.renderer!;
|
||
const elements = this.findDOMNodes(instance, selector);
|
||
if (!elements) {
|
||
return null;
|
||
}
|
||
|
||
const elems = elements.slice();
|
||
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, selector?: string): Array<Element | Text> | null {
|
||
const elements = this._renderer?.findDOMNodes(instance);
|
||
if (!elements) {
|
||
return null;
|
||
}
|
||
|
||
if (selector) {
|
||
const matched = getMatched(elements, selector);
|
||
if (!matched) {
|
||
return null;
|
||
}
|
||
return [matched];
|
||
}
|
||
return elements;
|
||
}
|
||
|
||
/**
|
||
* 通过 DOM 节点获取节点,依赖 simulator 的接口
|
||
*/
|
||
getNodeInstanceFromElement(target: Element | null): NodeInstance<ComponentInstance> | null {
|
||
if (!target) {
|
||
return null;
|
||
}
|
||
|
||
const nodeIntance = this.getClosestNodeInstance(target);
|
||
if (!nodeIntance) {
|
||
return null;
|
||
}
|
||
const { docId } = nodeIntance;
|
||
const doc = this.project.getDocument(docId)!;
|
||
const node = doc.getNode(nodeIntance.nodeId);
|
||
return {
|
||
...nodeIntance,
|
||
node,
|
||
};
|
||
}
|
||
|
||
private tryScrollAgain: number | null = null;
|
||
|
||
/**
|
||
* @see ISimulator
|
||
*/
|
||
/* istanbul ignore next */
|
||
scrollToNode(node: Node, detail?: any /* , tryTimes = 0 */) {
|
||
this.tryScrollAgain = null;
|
||
if (this.sensing) {
|
||
// active sensor
|
||
return;
|
||
}
|
||
|
||
const opt: any = {};
|
||
let scroll = false;
|
||
|
||
const componentInstance = this.getComponentInstances(detail?.near?.node || node)?.[0];
|
||
if (!componentInstance) return;
|
||
const domNode = this.findDOMNodes(componentInstance)?.[0] as Element;
|
||
if (!domNode) return;
|
||
if (isElementNode(domNode) && !isDOMNodeVisible(domNode, this.viewport)) {
|
||
const { left, top } = domNode.getBoundingClientRect();
|
||
const { scrollTop = 0, scrollLeft = 0 } = this.contentDocument?.documentElement || {};
|
||
opt.left = left + scrollLeft;
|
||
opt.top = top + scrollTop;
|
||
scroll = true;
|
||
}
|
||
/*
|
||
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) {
|
||
if (!isNaN(e.canvasX!) && !isNaN(e.canvasY!)) {
|
||
e.target = this.contentDocument?.elementFromPoint(e.canvasX!, e.canvasY!);
|
||
}
|
||
}
|
||
|
||
// 事件已订正
|
||
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: helper for locate ==========
|
||
|
||
/**
|
||
* @see ISensor
|
||
*/
|
||
locate(e: LocateEvent): any {
|
||
const { dragObject } = e;
|
||
const { nodes } = dragObject as DragNodeObject;
|
||
|
||
const operationalNodes = nodes?.filter((node) => {
|
||
const onMoveHook = node.componentMeta?.getMetadata()?.configure.advanced?.callbacks?.onMoveHook;
|
||
const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook(node.internalToShellNode()) : true;
|
||
|
||
let parentContainerNode: Node | null = null;
|
||
let parentNode = node.parent;
|
||
|
||
while (parentNode) {
|
||
if (parentNode.isContainer()) {
|
||
parentContainerNode = parentNode;
|
||
break;
|
||
}
|
||
|
||
parentNode = parentNode.parent;
|
||
}
|
||
|
||
const onChildMoveHook = parentContainerNode?.componentMeta?.getMetadata()?.configure.advanced?.callbacks?.onChildMoveHook;
|
||
|
||
const childrenCanMove = onChildMoveHook && parentContainerNode && typeof onChildMoveHook === 'function' ? onChildMoveHook(node.internalToShellNode(), parentContainerNode.internalToShellNode()) : true;
|
||
|
||
return canMove && childrenCanMove;
|
||
});
|
||
|
||
if (nodes && (!operationalNodes || operationalNodes.length === 0)) {
|
||
return;
|
||
}
|
||
|
||
this.sensing = true;
|
||
this.scroller.scrolling(e);
|
||
const document = this.project.currentDocument;
|
||
if (!document) {
|
||
return null;
|
||
}
|
||
const dropContainer = this.getDropContainer(e);
|
||
const lockedNode = getClosestNode(dropContainer?.container as Node, (node) => node.isLocked);
|
||
if (lockedNode) return null;
|
||
if (
|
||
!dropContainer
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
if (isLocationData(dropContainer)) {
|
||
return this.designer.createLocation(dropContainer);
|
||
}
|
||
|
||
const { container, instance: containerInstance } = dropContainer;
|
||
|
||
const edge = this.computeComponentInstanceRect(
|
||
containerInstance,
|
||
container.componentMeta.rootSelector,
|
||
);
|
||
|
||
if (!edge) {
|
||
return null;
|
||
}
|
||
|
||
const { children } = container;
|
||
|
||
const detail: LocationChildrenDetail = {
|
||
type: LocationDetailType.Children,
|
||
index: 0,
|
||
edge,
|
||
};
|
||
|
||
const locationData = {
|
||
target: container as ParentalNode,
|
||
detail,
|
||
source: `simulator${document.id}`,
|
||
event: e,
|
||
};
|
||
|
||
if (
|
||
e.dragObject &&
|
||
e.dragObject.nodes &&
|
||
e.dragObject.nodes.length &&
|
||
e.dragObject.nodes[0].componentMeta.isModal
|
||
) {
|
||
return this.designer.createLocation({
|
||
target: document.focusNode,
|
||
detail,
|
||
source: `simulator${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.rootSelector)
|
||
: 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;
|
||
|
||
// 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;
|
||
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 | null {
|
||
const { target, dragObject } = e;
|
||
const isAny = isDragAnyObject(dragObject);
|
||
const document = this.project.currentDocument!;
|
||
const { currentRoot } = 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 (!container.isParental()) {
|
||
container = container.parent || currentRoot;
|
||
}
|
||
|
||
// TODO: use spec container to accept specialData
|
||
if (isAny) {
|
||
// will return locationData
|
||
return null;
|
||
}
|
||
|
||
// get common parent, avoid drop container contains by dragObject
|
||
const drillDownExcludes = new Set<Node>();
|
||
if (isDragNodeObject(dragObject)) {
|
||
const { nodes } = dragObject;
|
||
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 || document.focusNode;
|
||
drillDownExcludes.add(container);
|
||
}
|
||
}
|
||
|
||
let instance: any;
|
||
if (nodeInstance) {
|
||
if (nodeInstance.node === container) {
|
||
instance = nodeInstance.instance;
|
||
} else {
|
||
instance = this.getClosestNodeInstance(
|
||
nodeInstance.instance as any,
|
||
container.id,
|
||
)?.instance;
|
||
}
|
||
} else {
|
||
instance = this.getComponentInstances(container)?.[0];
|
||
}
|
||
|
||
let dropContainer: DropContainer = {
|
||
container: container as any,
|
||
instance,
|
||
};
|
||
|
||
let res: any;
|
||
let upward: DropContainer | null = null;
|
||
while (container) {
|
||
res = this.handleAccept(dropContainer, e);
|
||
// if (isLocationData(res)) {
|
||
// return res;
|
||
// }
|
||
if (res === true) {
|
||
return dropContainer;
|
||
}
|
||
if (!res) {
|
||
drillDownExcludes.add(container);
|
||
if (upward) {
|
||
dropContainer = upward;
|
||
container = dropContainer.container;
|
||
upward = null;
|
||
} else if (container.parent) {
|
||
container = container.parent;
|
||
instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance;
|
||
dropContainer = {
|
||
container: container as ParentalNode,
|
||
instance,
|
||
};
|
||
} else {
|
||
return null;
|
||
}
|
||
} /* else if (res === DRILL_DOWN) {
|
||
if (!upward) {
|
||
container = container.parent;
|
||
instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance;
|
||
upward = {
|
||
container,
|
||
instance
|
||
};
|
||
}
|
||
dropContainer = this.getNearByContainer(dropContainer, drillDownExcludes, e);
|
||
if (!dropContainer) {
|
||
dropContainer = upward;
|
||
upward = null;
|
||
}
|
||
} else if (isNode(res)) {
|
||
// TODO:
|
||
} */
|
||
}
|
||
return null;
|
||
}
|
||
|
||
isAcceptable(/* container: ParentalNode */): 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): boolean {
|
||
const { dragObject } = e;
|
||
const document = this.currentDocument!;
|
||
const focusNode = document.focusNode;
|
||
if (isRootNode(container) || container.contains(focusNode)) {
|
||
return document.checkNesting(focusNode, dragObject as any);
|
||
}
|
||
|
||
const meta = (container as Node).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 document.checkNesting(container, dragObject as any);
|
||
}
|
||
|
||
/**
|
||
* 查找邻近容器
|
||
*/
|
||
getNearByContainer(
|
||
{ container, instance }: DropContainer,
|
||
drillDownExcludes: Set<Node>,
|
||
e: LocateEvent,
|
||
) {
|
||
const { children } = container;
|
||
const document = this.project.currentDocument!;
|
||
if (!children || children.isEmpty()) {
|
||
return null;
|
||
}
|
||
|
||
const nearDistance: any = null;
|
||
const nearBy: any = null;
|
||
for (let i = 0, l = children.size; i < l; i++) {
|
||
let child = children.get(i);
|
||
|
||
if (!child) {
|
||
continue;
|
||
}
|
||
if (child.conditionGroup) {
|
||
const bn = child.conditionGroup;
|
||
i = bn.index + bn.length - 1;
|
||
child = bn.visibleNode;
|
||
}
|
||
if (!child.isParental() || drillDownExcludes.has(child)) {
|
||
continue;
|
||
}
|
||
// TODO:
|
||
this.findDOMNodes(instance);
|
||
this.getComponentInstances(child);
|
||
const rect = this.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;
|
||
}
|