2020-03-30 17:41:31 +08:00

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;
}