import { obx, autorun, computed, getPublicPath, hotkey, focusTracker } from '@ali/lowcode-editor-core'; import { ISimulatorHost, Component, NodeInstance, ComponentInstance } from '../simulator'; import Viewport from './viewport'; import { createSimulator } from './create-simulator'; import { Node, ParentalNode, isNode, contains, isRootNode } from '../document'; import ResourceConsumer from './resource-consumer'; import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType, isElement, isFormEvent, hasOwnProperty, } from '@ali/lowcode-utils'; import { DragObjectType, isShaken, LocateEvent, isDragAnyObject, isDragNodeObject, LocationData, isLocationData, LocationChildrenDetail, LocationDetailType, isChildInline, isRowContainer, getRectTarget, Rect, CanvasPoint, Designer, } from '../designer'; import { parseMetadata } from './utils/parse-metadata'; import { ComponentMetadata, ComponentSchema } from '@ali/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'; export interface LibraryItem { package: string; library: string; urls?: Asset; } export interface BuiltinSimulatorProps { // 从 documentModel 上获取 // suspended?: boolean; designMode?: 'live' | 'design' | 'preview' | 'extend' | 'border'; device?: 'mobile' | 'iphone' | string; deviceClassName?: string; environment?: Asset; extraEnvironment?: Asset; library?: LibraryItem[]; simulatorUrl?: Asset; 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.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; 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 { readonly isSimulator = true; readonly project: Project; readonly designer: Designer; readonly viewport = new Viewport(); readonly scroller: Scroller; constructor(project: Project) { this.project = project; this.designer = project?.designer; this.scroller = this.designer.createScroller(this.viewport); } 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 deviceClassName(): string | undefined { return this.get('deviceClassName'); } @computed get designMode(): 'live' | 'design' | '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); } autorun(fn: (context: { dispose: () => void; firstRun: boolean }) => void) { return autorun(fn as any, true); } 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 componentsConsumer = new ResourceConsumer(() => 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; if (item.urls) { libraryAsset.push(item.urls); } }); } 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 onload // 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(); // bind hotkey & clipboard hotkey.mount(this._contentWindow); focusTracker.mount(this._contentWindow); clipboard.injectCopyPaster(this._contentDocument); // TODO: dispose the bindings } 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(); } setupDragAndClick() { const designer = this.designer; 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.selection; let isMulti = false; if (this.designMode === 'design') { isMulti = downEvent.metaKey || downEvent.ctrlKey; } else if (!downEvent.metaKey) { return; } // stop response document focus event downEvent.stopPropagation(); downEvent.preventDefault(); // FIXME: dirty fix remove label-for fro liveEditing (downEvent.target as HTMLElement).removeAttribute('for'); const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element); const node = nodeInst?.node || documentModel?.rootNode; // if (!node?.isValidComponent()) { // // 对于未注册组件直接返回 // return; // } const isLeftButton = downEvent.which === 1 || downEvent.button === 0; const checkSelect = (e: MouseEvent) => { doc.removeEventListener('mouseup', checkSelect, true); // 鼠标是否移动 if (!isShaken(downEvent, e)) { let id = node.id; designer.activeTracker.track({ node, instance: nodeInst?.instance }); if (isMulti && !isRootNode(node) && 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(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 && !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, instance: nodeInst?.instance }); selection.add(node.id); ignoreUpSelected = true; } selection.remove(documentModel.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) => { // fix for popups close logic const x = new Event('click'); x.initEvent('click', true); this._iframe?.dispatchEvent(x); const target = e.target as HTMLElement; // TODO: need more elegant solution to ignore click events of compoents in designer const ignoreSelectors: any = [ '.next-input-group', '.next-checkbox-group', '.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 ignoreSelectorsString = ignoreSelectors.join(','); if (isFormEvent(e) || target?.closest(ignoreSelectorsString)) { e.preventDefault(); e.stopPropagation(); } // stop response document click event // todo: catch link redirect }, true, ); } private disableDetecting?: () => void; /** * 设置悬停处理 */ setupDetecting() { const doc = this.contentDocument!; const detecting = this.designer.detecting; const hover = (e: MouseEvent) => { if (!detecting.enable || this.designMode !== 'design') { return; } const nodeInst = this.getNodeInstanceFromElement(e.target as Element); detecting.capture(nodeInst?.node || null); 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) => { 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 node = nodeInst.node || this.project.currentDocument?.rootNode; if (!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) { 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?.rootNode; 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('desiger.builtinSimulator.contextmenu', { selected, }); }); } /** * @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; } = {}; 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; let 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): object { throw new Error('Method not implemented.'); } /** * @see ISimulator */ getClosestNodeInstance(from: ComponentInstance, specId?: string): NodeInstance | 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 | 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 | 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 */ 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!); } // 事件已订正 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; const operationalNodes = nodes?.filter((node: any) => { const onMoveHook = node.componentMeta?.getMetadata()?.experimental?.callbacks?.onMoveHook; const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook() : true; return canMove; }); 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 canDropIn = dropContainer?.container?.componentMeta?.prototype?.options?.canDropIn; if ( !dropContainer || canDropIn === false || // too dirty (nodes && typeof canDropIn === 'function' && !canDropIn(operationalNodes[0])) ) { 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.children; 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.rootNode, 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.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 document = this.project.currentDocument!; const { currentRoot } = document; let container: Node; let nodeInstance: NodeInstance | 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(); 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 || document.rootNode; 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) { const { dragObject } = e; const document = this.currentDocument!; if (isRootNode(container)) { return document.checkDropTarget(container, 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, e: LocateEvent) { const children = container.children; const document = this.project.currentDocument!; if (!children || children.isEmpty()) { return null; } let nearDistance: any = null; let 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; } _innerWaitForCurrentDocument(): Promise { const timeGap = 200; return new Promise((resolve) => { setTimeout(() => { if (this.project.currentDocument) { resolve(); } }, timeGap); }).catch(() => { return this.waitForCurrentDocument(); }); } waitForCurrentDocument(): Promise { if (this.project.currentDocument) { return Promise.resolve(); } return this._innerWaitForCurrentDocument(); } // #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, 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: ParentalNode; instance: ComponentInstance; }