mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-02-21 08:20:27 +00:00
outline ok
This commit is contained in:
parent
9a2e5e97de
commit
4edf3fce6b
@ -20,4 +20,7 @@
|
|||||||
width: 3px;
|
width: 3px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
&.invalid {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Component } from 'react';
|
|||||||
import { computed, observer } from '../../../../../../globals';
|
import { computed, observer } from '../../../../../../globals';
|
||||||
import { SimulatorContext } from '../context';
|
import { SimulatorContext } from '../context';
|
||||||
import { SimulatorHost } from '../host';
|
import { SimulatorHost } from '../host';
|
||||||
import Location, {
|
import DropLocation, {
|
||||||
Rect,
|
Rect,
|
||||||
isLocationChildrenDetail,
|
isLocationChildrenDetail,
|
||||||
LocationChildrenDetail,
|
LocationChildrenDetail,
|
||||||
@ -23,16 +23,15 @@ interface InsertionData {
|
|||||||
/**
|
/**
|
||||||
* 处理拖拽子节点(INode)情况
|
* 处理拖拽子节点(INode)情况
|
||||||
*/
|
*/
|
||||||
function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: LocationChildrenDetail): InsertionData {
|
function processChildrenDetail(sim: ISimulator, container: NodeParent, detail: LocationChildrenDetail): InsertionData {
|
||||||
let edge = detail.edge || null;
|
let edge = detail.edge || null;
|
||||||
|
|
||||||
if (edge) {
|
if (!edge) {
|
||||||
edge = sim.computeRect(target);
|
edge = sim.computeRect(container);
|
||||||
}
|
|
||||||
|
|
||||||
if (!edge) {
|
if (!edge) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ret: any = {
|
const ret: any = {
|
||||||
edge,
|
edge,
|
||||||
@ -42,18 +41,33 @@ function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: Loca
|
|||||||
if (detail.near) {
|
if (detail.near) {
|
||||||
const { node, pos, rect, align } = detail.near;
|
const { node, pos, rect, align } = detail.near;
|
||||||
ret.nearRect = rect || sim.computeRect(node);
|
ret.nearRect = rect || sim.computeRect(node);
|
||||||
ret.vertical = align ? align === 'V' : isVertical(ret.nearRect);
|
if (pos === 'replace') {
|
||||||
|
// FIXME: ret.nearRect mybe null
|
||||||
|
ret.coverRect = ret.nearRect;
|
||||||
|
ret.insertType = 'cover';
|
||||||
|
} else if (!ret.nearRect || (ret.nearRect.width === 0 && ret.nearRect.height === 0)) {
|
||||||
|
ret.nearRect = ret.edge;
|
||||||
|
ret.insertType = 'after';
|
||||||
|
ret.vertical = isVertical(ret.nearRect);
|
||||||
|
} else {
|
||||||
ret.insertType = pos;
|
ret.insertType = pos;
|
||||||
|
ret.vertical = align ? align === 'V' : isVertical(ret.nearRect);
|
||||||
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// from outline-tree: has index, but no near
|
// from outline-tree: has index, but no near
|
||||||
// TODO: think of shadowNode & ConditionFlow
|
// TODO: think of shadowNode & ConditionFlow
|
||||||
const { index } = detail;
|
const { index } = detail;
|
||||||
let nearNode = target.children.get(index);
|
if (index == null) {
|
||||||
|
ret.coverRect = ret.edge;
|
||||||
|
ret.insertType = 'cover';
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
let nearNode = container.children.get(index);
|
||||||
if (!nearNode) {
|
if (!nearNode) {
|
||||||
// index = 0, eg. nochild,
|
// index = 0, eg. nochild,
|
||||||
nearNode = target.children.get(index > 0 ? index - 1 : 0);
|
nearNode = container.children.get(index > 0 ? index - 1 : 0);
|
||||||
if (!nearNode) {
|
if (!nearNode) {
|
||||||
ret.insertType = 'cover';
|
ret.insertType = 'cover';
|
||||||
ret.coverRect = edge;
|
ret.coverRect = edge;
|
||||||
@ -63,7 +77,14 @@ function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: Loca
|
|||||||
}
|
}
|
||||||
if (nearNode) {
|
if (nearNode) {
|
||||||
ret.nearRect = sim.computeRect(nearNode);
|
ret.nearRect = sim.computeRect(nearNode);
|
||||||
|
if (!ret.nearRect || (ret.nearRect.width === 0 && ret.nearRect.height === 0)) {
|
||||||
|
ret.nearRect = ret.edge;
|
||||||
|
ret.insertType = 'after';
|
||||||
|
}
|
||||||
ret.vertical = isVertical(ret.nearRect);
|
ret.vertical = isVertical(ret.nearRect);
|
||||||
|
} else {
|
||||||
|
ret.insertType = 'cover';
|
||||||
|
ret.coverRect = edge;
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -71,7 +92,7 @@ function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: Loca
|
|||||||
/**
|
/**
|
||||||
* 将 detail 信息转换为页面"坐标"信息
|
* 将 detail 信息转换为页面"坐标"信息
|
||||||
*/
|
*/
|
||||||
function processDetail({ target, detail, document }: Location): InsertionData {
|
function processDetail({ target, detail, document }: DropLocation): InsertionData {
|
||||||
const sim = document.simulator;
|
const sim = document.simulator;
|
||||||
if (!sim) {
|
if (!sim) {
|
||||||
return {};
|
return {};
|
||||||
@ -115,6 +136,9 @@ export class InsertionView extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let className = 'lc-insertion';
|
let className = 'lc-insertion';
|
||||||
|
if ((loc.detail as any)?.valid === false) {
|
||||||
|
className += ' invalid';
|
||||||
|
}
|
||||||
const style: any = {};
|
const style: any = {};
|
||||||
let x: number;
|
let x: number;
|
||||||
let y: number;
|
let y: number;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ISimulator, Component, NodeInstance } from '../../../designer/simulator
|
|||||||
import Viewport from './viewport';
|
import Viewport from './viewport';
|
||||||
import { createSimulator } from './create-simulator';
|
import { createSimulator } from './create-simulator';
|
||||||
import { SimulatorRenderer } from '../renderer/renderer';
|
import { SimulatorRenderer } from '../renderer/renderer';
|
||||||
import Node, { NodeParent, isNodeParent, isNode, contains } from '../../../designer/document/node/node';
|
import Node, { NodeParent, isNodeParent, isNode, contains, PositionNO } from '../../../designer/document/node/node';
|
||||||
import DocumentModel from '../../../designer/document/document-model';
|
import DocumentModel from '../../../designer/document/document-model';
|
||||||
import ResourceConsumer from './resource-consumer';
|
import ResourceConsumer from './resource-consumer';
|
||||||
import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType } from '../utils/asset';
|
import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType } from '../utils/asset';
|
||||||
@ -536,7 +536,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
/**
|
/**
|
||||||
* 通过 DOM 节点获取节点,依赖 simulator 的接口
|
* 通过 DOM 节点获取节点,依赖 simulator 的接口
|
||||||
*/
|
*/
|
||||||
getNodeInstanceFromElement(target: Element | null): NodeInstance | null {
|
getNodeInstanceFromElement(target: Element | null): NodeInstance<ReactInstance> | null {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -701,31 +701,24 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
locate(e: LocateEvent): any {
|
locate(e: LocateEvent): any {
|
||||||
this.sensing = true;
|
this.sensing = true;
|
||||||
this.scroller.scrolling(e);
|
this.scroller.scrolling(e);
|
||||||
const dropTarget = this.getDropTarget(e);
|
const dropContainer = this.getDropContainer(e);
|
||||||
if (!dropTarget) {
|
if (!dropContainer) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLocationData(dropTarget)) {
|
if (isLocationData(dropContainer)) {
|
||||||
return this.designer.createLocation(dropTarget);
|
return this.designer.createLocation(dropContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = dropTarget;
|
const { container, instance: containerInstance } = dropContainer;
|
||||||
|
|
||||||
// FIXME: e.target is #document, etc., does not has e.targetInstance
|
const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rectSelector);
|
||||||
|
|
||||||
const targetInstance = e.targetInstance as ReactInstance;
|
|
||||||
const parentInstance = this.getClosestNodeInstance(targetInstance, target.id);
|
|
||||||
const edge = this.computeComponentInstanceRect(
|
|
||||||
parentInstance?.instance as any,
|
|
||||||
parentInstance?.node?.componentMeta.rectSelector,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!edge) {
|
if (!edge) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = target.children;
|
const children = container.children;
|
||||||
|
|
||||||
const detail: LocationChildrenDetail = {
|
const detail: LocationChildrenDetail = {
|
||||||
type: LocationDetailType.Children,
|
type: LocationDetailType.Children,
|
||||||
@ -734,8 +727,10 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const locationData = {
|
const locationData = {
|
||||||
target,
|
target: container,
|
||||||
detail,
|
detail,
|
||||||
|
source: 'simulator' + this.document.id,
|
||||||
|
event: e,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!children || children.size < 1 || !edge) {
|
if (!children || children.size < 1 || !edge) {
|
||||||
@ -755,7 +750,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
const instances = this.getComponentInstances(node);
|
const instances = this.getComponentInstances(node);
|
||||||
const inst = instances
|
const inst = instances
|
||||||
? instances.length > 1
|
? instances.length > 1
|
||||||
? instances.find(inst => this.getClosestNodeInstance(inst, target.id)?.instance === targetInstance)
|
? instances.find(inst => this.getClosestNodeInstance(inst, container.id)?.instance === containerInstance)
|
||||||
: instances[0]
|
: instances[0]
|
||||||
: null;
|
: null;
|
||||||
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null;
|
const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null;
|
||||||
@ -830,61 +825,109 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
return this.designer.createLocation(locationData);
|
return this.designer.createLocation(locationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDropTarget(e: LocateEvent): NodeParent | LocationData | null {
|
/**
|
||||||
|
* 查找合适的投放容器
|
||||||
|
*/
|
||||||
|
getDropContainer(e: LocateEvent): DropContainer | LocationData | null {
|
||||||
const { target, dragObject } = e;
|
const { target, dragObject } = e;
|
||||||
const isAny = isDragAnyObject(dragObject);
|
const isAny = isDragAnyObject(dragObject);
|
||||||
let container: any;
|
const { modalNode, currentRoot } = this.document;
|
||||||
|
let container: Node;
|
||||||
|
let nodeInstance: NodeInstance<ReactInstance> | undefined;
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
const ref = this.getNodeInstanceFromElement(target);
|
const ref = this.getNodeInstanceFromElement(target);
|
||||||
if (ref?.node) {
|
if (ref?.node) {
|
||||||
e.targetInstance = ref.instance;
|
nodeInstance = ref;
|
||||||
e.targetNode = ref.node;
|
|
||||||
container = ref.node;
|
container = ref.node;
|
||||||
} else if (isAny) {
|
} else if (isAny) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
container = this.document.rootNode;
|
container = currentRoot;
|
||||||
}
|
}
|
||||||
} else if (isAny) {
|
} else if (isAny) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
container = this.document.rootNode;
|
container = currentRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNodeParent(container) && !isRootNode(container)) {
|
if (!isNodeParent(container)) {
|
||||||
container = container.parent;
|
container = container.parent || currentRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check container if in modalNode layer, if not, use modalNode
|
||||||
|
if (modalNode && !modalNode.contains(container)) {
|
||||||
|
container = modalNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAny) {
|
|
||||||
// TODO: use spec container to accept specialData
|
// TODO: use spec container to accept specialData
|
||||||
|
if (isAny) {
|
||||||
|
// will return locationData
|
||||||
return null;
|
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 res: any;
|
||||||
let upward: any;
|
let upward: any;
|
||||||
// TODO: improve AT_CHILD logic, mark has checked
|
// TODO: complete drill down logic
|
||||||
while (container) {
|
while (container) {
|
||||||
res = this.acceptNodes(container, e);
|
if (ret.container !== container) {
|
||||||
|
ret.container = container;
|
||||||
|
ret.instance = this.getClosestNodeInstance(ret.instance, container.id)?.instance;
|
||||||
|
}
|
||||||
|
res = this.handleAccept(ret, e);
|
||||||
if (isLocationData(res)) {
|
if (isLocationData(res)) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
if (res === true) {
|
if (res === true) {
|
||||||
return container;
|
return ret;
|
||||||
}
|
}
|
||||||
if (!res) {
|
if (!res) {
|
||||||
|
drillDownExcludes.add(container);
|
||||||
if (upward) {
|
if (upward) {
|
||||||
container = upward;
|
container = upward;
|
||||||
upward = null;
|
upward = null;
|
||||||
} else {
|
} else if (container.parent) {
|
||||||
container = container.parent;
|
container = container.parent;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} else if (isNode(res)) {
|
} else if (isNode(res)) {
|
||||||
/* else if (res === AT_CHILD) {
|
/* else if (res === DRILL_DOWN) {
|
||||||
if (!upward) {
|
if (!upward) {
|
||||||
upward = container.parent;
|
upward = container.parent;
|
||||||
}
|
}
|
||||||
container = this.getNearByContainer(container, e);
|
container = this.getNearByContainer(container, drillExcludes, e);
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = upward;
|
container = upward;
|
||||||
upward = null;
|
upward = null;
|
||||||
@ -897,38 +940,71 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptNodes(container: NodeParent, e: LocateEvent) {
|
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;
|
const { dragObject } = e;
|
||||||
if (isRootNode(container)) {
|
if (isRootNode(container)) {
|
||||||
return this.checkDropTarget(container, dragObject as any);
|
return this.document.checkDropTarget(container, dragObject as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = container.componentMeta;
|
const meta = container.componentMeta;
|
||||||
|
|
||||||
if (!config.isContainer) {
|
// FIXME: get containerInstance for accept logic use
|
||||||
|
const acceptable: boolean = this.isAcceptable(container);
|
||||||
|
if (!meta.isContainer && !acceptable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// check is contains, get common parent
|
|
||||||
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) {
|
|
||||||
return p || this.document.rootNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.checkNesting(container, dragObject as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// first use accept
|
||||||
|
if (acceptable) {
|
||||||
/*
|
/*
|
||||||
getNearByContainer(container: NodeParent, e: LocateEvent) {
|
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;
|
const children = container.children;
|
||||||
if (!children || children.length < 1) {
|
if (!children || children.length < 1) {
|
||||||
return null;
|
return null;
|
||||||
@ -963,43 +1039,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nearBy;
|
return nearBy;
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
checkNesting(dropTarget: NodeParent, dragObject: DragNodeObject | DragNodeDataObject): boolean {
|
|
||||||
let items: Array<Node | NodeSchema>;
|
|
||||||
if (isDragNodeDataObject(dragObject)) {
|
|
||||||
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
|
||||||
} else {
|
|
||||||
items = dragObject.nodes;
|
|
||||||
}
|
|
||||||
return items.every(item => this.checkNestingDown(dropTarget, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
checkDropTarget(dropTarget: NodeParent, dragObject: DragNodeObject | DragNodeDataObject): boolean {
|
|
||||||
let items: Array<Node | NodeSchema>;
|
|
||||||
if (isDragNodeDataObject(dragObject)) {
|
|
||||||
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
|
||||||
} else {
|
|
||||||
items = dragObject.nodes;
|
|
||||||
}
|
|
||||||
return items.every(item => this.checkNestingUp(dropTarget, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNestingUp(parent: NodeParent, target: NodeSchema | Node): boolean {
|
|
||||||
if (isNode(target) || isNodeSchema(target)) {
|
|
||||||
const config = isNode(target) ? target.componentMeta : this.document.getComponentMeta(target.componentName);
|
|
||||||
if (config) {
|
|
||||||
return config.checkNestingUp(target, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNestingDown(parent: NodeParent, target: NodeSchema | Node): boolean {
|
|
||||||
const config = parent.componentMeta;
|
|
||||||
return config.checkNestingDown(parent, target) && this.checkNestingUp(parent, target);
|
|
||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
}
|
}
|
||||||
@ -1065,3 +1105,8 @@ function getMatched(elements: Array<Element | Text>, selector: string): Element
|
|||||||
}
|
}
|
||||||
return firstQueried;
|
return firstQueried;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DropContainer {
|
||||||
|
container: NodeParent;
|
||||||
|
instance: ReactInstance;
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Project from './project';
|
|||||||
import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './helper/dragon';
|
import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './helper/dragon';
|
||||||
import ActiveTracker from './helper/active-tracker';
|
import ActiveTracker from './helper/active-tracker';
|
||||||
import Hovering from './helper/hovering';
|
import Hovering from './helper/hovering';
|
||||||
import Location, { LocationData, isLocationChildrenDetail } from './helper/location';
|
import DropLocation, { LocationData, isLocationChildrenDetail } from './helper/location';
|
||||||
import DocumentModel from './document/document-model';
|
import DocumentModel from './document/document-model';
|
||||||
import Node, { insertChildren } from './document/node/node';
|
import Node, { insertChildren } from './document/node/node';
|
||||||
import { isRootNode } from './document/node/root-node';
|
import { isRootNode } from './document/node/root-node';
|
||||||
@ -30,7 +30,7 @@ export interface DesignerProps {
|
|||||||
onMount?: (designer: Designer) => void;
|
onMount?: (designer: Designer) => void;
|
||||||
onDragstart?: (e: LocateEvent) => void;
|
onDragstart?: (e: LocateEvent) => void;
|
||||||
onDrag?: (e: LocateEvent) => void;
|
onDrag?: (e: LocateEvent) => void;
|
||||||
onDragend?: (e: { dragObject: DragObject; copy: boolean }, loc?: Location) => void;
|
onDragend?: (e: { dragObject: DragObject; copy: boolean }, loc?: DropLocation) => void;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,12 +158,12 @@ export default class Designer {
|
|||||||
this.props?.eventPipe?.emit(`designer.${event}`, ...args);
|
this.props?.eventPipe?.emit(`designer.${event}`, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dropLocation?: Location;
|
private _dropLocation?: DropLocation;
|
||||||
/**
|
/**
|
||||||
* 创建插入位置,考虑放到 dragon 中
|
* 创建插入位置,考虑放到 dragon 中
|
||||||
*/
|
*/
|
||||||
createLocation(locationData: LocationData): Location {
|
createLocation(locationData: LocationData): DropLocation {
|
||||||
const loc = new Location(locationData);
|
const loc = new DropLocation(locationData);
|
||||||
if (this._dropLocation && this._dropLocation.document !== loc.document) {
|
if (this._dropLocation && this._dropLocation.document !== loc.document) {
|
||||||
this._dropLocation.document.internalSetDropLocation(null);
|
this._dropLocation.document.internalSetDropLocation(null);
|
||||||
}
|
}
|
||||||
@ -290,7 +290,7 @@ export default class Designer {
|
|||||||
return this.project.schema;
|
return this.project.schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
set schema(schema: ProjectSchema) {
|
setSchema(schema: ProjectSchema) {
|
||||||
// todo:
|
// todo:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import Project from '../project';
|
import Project from '../project';
|
||||||
import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './node/node';
|
import Node, { isNodeParent, insertChildren, insertChild, NodeParent, isNode } from './node/node';
|
||||||
import { Selection } from './selection';
|
import { Selection } from './selection';
|
||||||
import RootNode from './node/root-node';
|
import RootNode from './node/root-node';
|
||||||
import { ISimulator } from '../simulator';
|
import { ISimulator } from '../simulator';
|
||||||
import Location from '../helper/location';
|
import DropLocation from '../helper/location';
|
||||||
import { ComponentMeta } from '../component-meta';
|
import { ComponentMeta } from '../component-meta';
|
||||||
import History from '../helper/history';
|
import History from '../helper/history';
|
||||||
import Prop from './node/props/prop';
|
import Prop from './node/props/prop';
|
||||||
@ -16,7 +16,9 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
obx,
|
obx,
|
||||||
autorun,
|
autorun,
|
||||||
|
isNodeSchema,
|
||||||
} from '../../../../globals';
|
} from '../../../../globals';
|
||||||
|
import { isDragNodeDataObject, DragNodeObject, DragNodeDataObject } from '../helper/dragon';
|
||||||
|
|
||||||
export default class DocumentModel {
|
export default class DocumentModel {
|
||||||
/**
|
/**
|
||||||
@ -56,6 +58,15 @@ export default class DocumentModel {
|
|||||||
this.rootNode.getExtraProp('fileName', true)?.setValue(fileName);
|
this.rootNode.getExtraProp('fileName', true)?.setValue(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _modalNode?: NodeParent;
|
||||||
|
get modalNode() {
|
||||||
|
return this._modalNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentRoot() {
|
||||||
|
return this.modalNode || this.rootNode;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(readonly project: Project, schema: RootSchema) {
|
constructor(readonly project: Project, schema: RootSchema) {
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
this.nodes.forEach(item => {
|
this.nodes.forEach(item => {
|
||||||
@ -201,11 +212,11 @@ export default class DocumentModel {
|
|||||||
node.remove();
|
node.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@obx.ref private _dropLocation: Location | null = null;
|
@obx.ref private _dropLocation: DropLocation | null = null;
|
||||||
/**
|
/**
|
||||||
* 内部方法,请勿调用
|
* 内部方法,请勿调用
|
||||||
*/
|
*/
|
||||||
internalSetDropLocation(loc: Location | null) {
|
internalSetDropLocation(loc: DropLocation | null) {
|
||||||
this._dropLocation = loc;
|
this._dropLocation = loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,6 +389,48 @@ export default class DocumentModel {
|
|||||||
remove() {
|
remove() {
|
||||||
// todo:
|
// todo:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkNesting(dropTarget: NodeParent, dragObject: DragNodeObject | DragNodeDataObject): boolean {
|
||||||
|
let items: Array<Node | NodeSchema>;
|
||||||
|
if (isDragNodeDataObject(dragObject)) {
|
||||||
|
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
||||||
|
} else {
|
||||||
|
items = dragObject.nodes;
|
||||||
|
}
|
||||||
|
return items.every(item => this.checkNestingDown(dropTarget, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDropTarget(dropTarget: NodeParent, dragObject: DragNodeObject | DragNodeDataObject): boolean {
|
||||||
|
let items: Array<Node | NodeSchema>;
|
||||||
|
if (isDragNodeDataObject(dragObject)) {
|
||||||
|
items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data];
|
||||||
|
} else {
|
||||||
|
items = dragObject.nodes;
|
||||||
|
}
|
||||||
|
return items.every(item => this.checkNestingUp(dropTarget, item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查对象对父级的要求,涉及配置 parentWhitelist
|
||||||
|
*/
|
||||||
|
checkNestingUp(parent: NodeParent, obj: NodeSchema | Node): boolean {
|
||||||
|
if (isNode(obj) || isNodeSchema(obj)) {
|
||||||
|
const config = isNode(obj) ? obj.componentMeta : this.getComponentMeta(obj.componentName);
|
||||||
|
if (config) {
|
||||||
|
return config.checkNestingUp(obj, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查投放位置对子级的要求,涉及配置 childWhitelist
|
||||||
|
*/
|
||||||
|
checkNestingDown(parent: NodeParent, obj: NodeSchema | Node): boolean {
|
||||||
|
const config = parent.componentMeta;
|
||||||
|
return config.checkNestingDown(parent, obj) && this.checkNestingUp(parent, obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDocumentModel(obj: any): obj is DocumentModel {
|
export function isDocumentModel(obj: any): obj is DocumentModel {
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import { uniqueId } from '../../../../../utils/unique-id';
|
|||||||
import Node from './node';
|
import Node from './node';
|
||||||
import { intl } from '../../../locale';
|
import { intl } from '../../../locale';
|
||||||
|
|
||||||
|
// modals assoc x-hide value, initial: check is Modal, yes will put it in modals, cross levels
|
||||||
|
// if-else-if assoc conditionGroup value, should be the same level, and siblings, need renderEngine support
|
||||||
export default class ExclusiveGroup {
|
export default class ExclusiveGroup {
|
||||||
readonly isExclusiveGroup = true;
|
readonly isExclusiveGroup = true;
|
||||||
readonly id = uniqueId('cond-grp');
|
readonly id = uniqueId('exclusive');
|
||||||
@obx.val readonly children: Node[] = [];
|
@obx.val readonly children: Node[] = [];
|
||||||
|
|
||||||
@obx private visibleIndex = 0;
|
@obx private visibleIndex = 0;
|
||||||
|
|||||||
@ -39,11 +39,11 @@ export default class NodeChildren {
|
|||||||
} else {
|
} else {
|
||||||
node = this.owner.document.createNode(item);
|
node = this.owner.document.createNode(item);
|
||||||
}
|
}
|
||||||
node.internalSetParent(this.owner);
|
|
||||||
children[i] = node;
|
children[i] = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.children = children;
|
this.children = children;
|
||||||
|
this.interalInitParent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -28,12 +28,13 @@ import ExclusiveGroup, { isExclusiveGroup } from './exclusive-group';
|
|||||||
* loop
|
* loop
|
||||||
* loopArgs
|
* loopArgs
|
||||||
* condition
|
* condition
|
||||||
* ------- future support -----
|
* ------- addition support -----
|
||||||
* conditionGroup
|
* conditionGroup use for condition, for exclusive
|
||||||
* title
|
* title display on outline
|
||||||
* ignored
|
* ignored ignore this node will not publish to render, but will store
|
||||||
* locked
|
* locked can not select/hover/ item on canvas but can control on outline
|
||||||
* hidden
|
* hidden not visible on canvas
|
||||||
|
* slotArgs like loopArgs, for slot node
|
||||||
*/
|
*/
|
||||||
export default class Node {
|
export default class Node {
|
||||||
/**
|
/**
|
||||||
@ -49,11 +50,12 @@ export default class Node {
|
|||||||
/**
|
/**
|
||||||
* 节点组件类型
|
* 节点组件类型
|
||||||
* 特殊节点:
|
* 特殊节点:
|
||||||
* * #text 文字节点
|
|
||||||
* * #expression 表达式节点
|
|
||||||
* * Page 页面
|
* * Page 页面
|
||||||
* * Block/Fragment 区块
|
* * Block 区块
|
||||||
* * Component 组件/元件
|
* * Component 组件/元件
|
||||||
|
* * Fragment 碎片节点,无 props,有指令
|
||||||
|
* * Leaf 文字节点 | 表达式节点,无 props,无指令?
|
||||||
|
* * Slot 插槽节点,无 props,正常 children,有 slotArgs,有指令
|
||||||
*/
|
*/
|
||||||
readonly componentName: string;
|
readonly componentName: string;
|
||||||
/**
|
/**
|
||||||
@ -240,6 +242,8 @@ export default class Node {
|
|||||||
if (!isExclusiveGroup(grp)) {
|
if (!isExclusiveGroup(grp)) {
|
||||||
if (this.prevSibling?.conditionGroup?.name === grp) {
|
if (this.prevSibling?.conditionGroup?.name === grp) {
|
||||||
grp = this.prevSibling.conditionGroup;
|
grp = this.prevSibling.conditionGroup;
|
||||||
|
} else if (this.nextSibling?.conditionGroup?.name === grp) {
|
||||||
|
grp = this.nextSibling.conditionGroup;
|
||||||
} else {
|
} else {
|
||||||
grp = new ExclusiveGroup(grp);
|
grp = new ExclusiveGroup(grp);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import Location from './location';
|
import DropLocation from './location';
|
||||||
import DocumentModel from '../document/document-model';
|
import DocumentModel from '../document/document-model';
|
||||||
import { ISimulator, isSimulator, ComponentInstance } from '../simulator';
|
import { ISimulator, isSimulator, ComponentInstance } from '../simulator';
|
||||||
import Node from '../document/node/node';
|
import Node from '../document/node/node';
|
||||||
@ -47,9 +47,6 @@ export interface LocateEvent {
|
|||||||
* 事件订正标识,初始构造时,从发起端构造,缺少 canvasX,canvasY, 需要经过订正才有
|
* 事件订正标识,初始构造时,从发起端构造,缺少 canvasX,canvasY, 需要经过订正才有
|
||||||
*/
|
*/
|
||||||
fixed?: true;
|
fixed?: true;
|
||||||
|
|
||||||
targetNode?: Node;
|
|
||||||
targetInstance?: ComponentInstance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +64,7 @@ export interface ISensor {
|
|||||||
/**
|
/**
|
||||||
* 定位并激活
|
* 定位并激活
|
||||||
*/
|
*/
|
||||||
locate(e: LocateEvent): Location | undefined;
|
locate(e: LocateEvent): DropLocation | undefined | null;
|
||||||
/**
|
/**
|
||||||
* 是否进入敏感板区域
|
* 是否进入敏感板区域
|
||||||
*/
|
*/
|
||||||
@ -204,10 +201,10 @@ export default class Dragon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private emitter = new EventEmitter();
|
private emitter = new EventEmitter();
|
||||||
private emptyImage: HTMLImageElement = new Image();
|
// private emptyImage: HTMLImageElement = new Image();
|
||||||
|
|
||||||
constructor(readonly designer: Designer) {
|
constructor(readonly designer: Designer) {
|
||||||
this.emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
// this.emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||||
}
|
}
|
||||||
|
|
||||||
from(shell: Element, boost: (e: MouseEvent) => DragObject | null) {
|
from(shell: Element, boost: (e: MouseEvent) => DragObject | null) {
|
||||||
@ -280,6 +277,7 @@ export default class Dragon {
|
|||||||
|
|
||||||
let lastArrive: any;
|
let lastArrive: any;
|
||||||
const drag = (e: MouseEvent | DragEvent) => {
|
const drag = (e: MouseEvent | DragEvent) => {
|
||||||
|
// FIXME: donot setcopy when: newbie & no location
|
||||||
checkcopy(e);
|
checkcopy(e);
|
||||||
|
|
||||||
if (isInvalidPoint(e, lastArrive)) return;
|
if (isInvalidPoint(e, lastArrive)) return;
|
||||||
@ -433,6 +431,7 @@ export default class Dragon {
|
|||||||
const chooseSensor = (e: LocateEvent) => {
|
const chooseSensor = (e: LocateEvent) => {
|
||||||
let sensor = e.sensor && e.sensor.isEnter(e) ? e.sensor : sensors.find(s => s.sensorAvailable && s.isEnter(e));
|
let sensor = e.sensor && e.sensor.isEnter(e) ? e.sensor : sensors.find(s => s.sensorAvailable && s.isEnter(e));
|
||||||
if (!sensor) {
|
if (!sensor) {
|
||||||
|
// TODO: enter some area like componentspanel cancel
|
||||||
if (lastSensor) {
|
if (lastSensor) {
|
||||||
sensor = lastSensor;
|
sensor = lastSensor;
|
||||||
} else if (e.sensor) {
|
} else if (e.sensor) {
|
||||||
@ -539,13 +538,6 @@ export default class Dragon {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否拷贝态
|
|
||||||
*/
|
|
||||||
private isCopyState(): boolean {
|
|
||||||
return cursor.isCopy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除所有态:拖拽态、拷贝态
|
* 清除所有态:拖拽态、拷贝态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import ComponentNode, { NodeParent } from '../document/node/node';
|
import ComponentNode, { NodeParent } from '../document/node/node';
|
||||||
import DocumentModel from '../document/document-model';
|
import DocumentModel from '../document/document-model';
|
||||||
|
import { LocateEvent } from './dragon';
|
||||||
|
|
||||||
export interface LocationData {
|
export interface LocationData {
|
||||||
target: NodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode
|
target: NodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode
|
||||||
detail: LocationDetail;
|
detail: LocationDetail;
|
||||||
|
source: string;
|
||||||
|
event: LocateEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LocationDetailType {
|
export enum LocationDetailType {
|
||||||
@ -13,14 +16,19 @@ export enum LocationDetailType {
|
|||||||
|
|
||||||
export interface LocationChildrenDetail {
|
export interface LocationChildrenDetail {
|
||||||
type: LocationDetailType.Children;
|
type: LocationDetailType.Children;
|
||||||
index: number;
|
index?: number | null;
|
||||||
|
/**
|
||||||
|
* 是否有效位置
|
||||||
|
*/
|
||||||
|
valid?: boolean;
|
||||||
edge?: DOMRect;
|
edge?: DOMRect;
|
||||||
near?: {
|
near?: {
|
||||||
node: ComponentNode;
|
node: ComponentNode;
|
||||||
pos: 'before' | 'after';
|
pos: 'before' | 'after' | 'replace';
|
||||||
rect?: Rect;
|
rect?: Rect;
|
||||||
align?: 'V' | 'H';
|
align?: 'V' | 'H';
|
||||||
};
|
};
|
||||||
|
focus?: { type: 'slots' } | { type: 'node'; node: NodeParent };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocationPropDetail {
|
export interface LocationPropDetail {
|
||||||
@ -118,15 +126,28 @@ export function getWindow(elem: Element | Document): Window {
|
|||||||
return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!;
|
return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Location {
|
export default class DropLocation {
|
||||||
readonly target: NodeParent;
|
readonly target: NodeParent;
|
||||||
readonly detail: LocationDetail;
|
readonly detail: LocationDetail;
|
||||||
|
readonly event: LocateEvent;
|
||||||
|
readonly source: string;
|
||||||
get document(): DocumentModel {
|
get document(): DocumentModel {
|
||||||
return this.target.document;
|
return this.target.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ target, detail }: LocationData) {
|
constructor({ target, detail, source, event }: LocationData) {
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.detail = detail;
|
this.detail = detail;
|
||||||
|
this.source = source;
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(event: LocateEvent): DropLocation {
|
||||||
|
return new DropLocation({
|
||||||
|
target: this.target,
|
||||||
|
detail: this.detail,
|
||||||
|
source: this.source,
|
||||||
|
event,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,8 +43,8 @@ const SCROLL_ACCURCY = 30;
|
|||||||
|
|
||||||
export interface IScrollable {
|
export interface IScrollable {
|
||||||
scrollTarget?: ScrollTarget | Element;
|
scrollTarget?: ScrollTarget | Element;
|
||||||
bounds: DOMRect;
|
bounds?: DOMRect | null;
|
||||||
scale: number;
|
scale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Scroller {
|
export default class Scroller {
|
||||||
@ -62,8 +62,7 @@ export default class Scroller {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private scrollable: IScrollable) {
|
constructor(private scrollable: IScrollable) {}
|
||||||
}
|
|
||||||
|
|
||||||
scrollTo(options: { left?: number; top?: number }) {
|
scrollTo(options: { left?: number; top?: number }) {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
@ -119,9 +118,9 @@ export default class Scroller {
|
|||||||
scrolling(point: { globalX: number; globalY: number }) {
|
scrolling(point: { globalX: number; globalY: number }) {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
|
|
||||||
const { bounds, scale } = this.scrollable;
|
const { bounds, scale = 1 } = this.scrollable;
|
||||||
const scrollTarget = this.scrollTarget;
|
const scrollTarget = this.scrollTarget;
|
||||||
if (!scrollTarget) {
|
if (!scrollTarget || !bounds) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -165,7 +165,7 @@ export default {
|
|||||||
name: 'children',
|
name: 'children',
|
||||||
propType: 'node'
|
propType: 'node'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'Button.Group': {
|
'Button.Group': {
|
||||||
componentName: 'Button.Group',
|
componentName: 'Button.Group',
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './create-icon';
|
export * from './create-icon';
|
||||||
export * from './is-react';
|
export * from './is-react';
|
||||||
|
export * from './unique-id';
|
||||||
|
|||||||
4
packages/globals/src/utils/unique-id.ts
Normal file
4
packages/globals/src/utils/unique-id.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
let guid = Date.now();
|
||||||
|
export function uniqueId(prefix = '') {
|
||||||
|
return `${prefix}${(guid++).toString(36).toLowerCase()}`;
|
||||||
|
}
|
||||||
@ -1,37 +1,52 @@
|
|||||||
|
import { NodeParent } from '../../../designer/src/designer/document/node/node';
|
||||||
|
import DropLocation, { isLocationChildrenDetail } from '../../../designer/src/designer/helper/location';
|
||||||
|
import { LocateEvent } from '../../../designer/src/designer/helper/dragon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停留检查计时器
|
* 停留检查计时器
|
||||||
*/
|
*/
|
||||||
export default class DwellTimer {
|
export default class DwellTimer {
|
||||||
private timer: number | undefined;
|
private timer: number | undefined;
|
||||||
private previous: any;
|
private previous?: NodeParent;
|
||||||
|
private event?: LocateEvent
|
||||||
|
|
||||||
constructor(readonly timeout: number = 400) {}
|
constructor(private decide: (node: NodeParent, event: LocateEvent) => void, private timeout: number = 800) {}
|
||||||
|
|
||||||
/**
|
focus(node: NodeParent, event: LocateEvent) {
|
||||||
* 根据传入的 ID 判断,停留事件是否触发
|
this.event = event;
|
||||||
* 如果上一次的标示(包括不存在)和这次不相同,则设置停留计时器
|
if (this.previous === node) {
|
||||||
* 反之什么也不用做
|
return;
|
||||||
*/
|
}
|
||||||
start(id: any, fn: () => void) {
|
this.reset();
|
||||||
if (this.previous !== id) {
|
this.previous = node;
|
||||||
this.end();
|
const x = Date.now();
|
||||||
this.previous = id;
|
console.info('set', x);
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
fn();
|
console.info('done', x, Date.now() - x);
|
||||||
this.end();
|
this.previous && this.decide(this.previous, this.event!);
|
||||||
}, this.timeout) as number;
|
this.reset();
|
||||||
|
}, this.timeout) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryFocus(loc?: DropLocation | null) {
|
||||||
|
if (!loc || !isLocationChildrenDetail(loc.detail)) {
|
||||||
|
this.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loc.detail.focus?.type === 'node') {
|
||||||
|
this.focus(loc.detail.focus.node, loc.event);
|
||||||
|
} else {
|
||||||
|
this.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
reset() {
|
||||||
const timer = this.timer;
|
console.info('reset');
|
||||||
if (timer) {
|
if (this.timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = undefined;
|
this.timer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.previous) {
|
|
||||||
this.previous = undefined;
|
this.previous = undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
53
packages/plugin-outline-tree/src/helper/indent-track.ts
Normal file
53
packages/plugin-outline-tree/src/helper/indent-track.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import DropLocation, { isLocationChildrenDetail } from '../../../designer/src/designer/helper/location';
|
||||||
|
import { NodeParent } from '../../../designer/src/designer/document/node/node';
|
||||||
|
|
||||||
|
const IndentSensitive = 15;
|
||||||
|
export class IndentTrack {
|
||||||
|
private indentStart: number | null = null;
|
||||||
|
reset() {
|
||||||
|
this.indentStart = null;
|
||||||
|
}
|
||||||
|
getIndentParent(lastLoc: DropLocation, loc: DropLocation): [NodeParent, number] | null {
|
||||||
|
if (
|
||||||
|
lastLoc.target !== loc.target ||
|
||||||
|
!isLocationChildrenDetail(lastLoc.detail) ||
|
||||||
|
!isLocationChildrenDetail(loc.detail) ||
|
||||||
|
lastLoc.source !== loc.source ||
|
||||||
|
lastLoc.detail.index !== loc.detail.index ||
|
||||||
|
loc.detail.index == null
|
||||||
|
) {
|
||||||
|
this.indentStart = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (this.indentStart == null) {
|
||||||
|
this.indentStart = lastLoc.event.globalX;
|
||||||
|
}
|
||||||
|
const delta = loc.event.globalX - this.indentStart;
|
||||||
|
const indent = Math.floor(Math.abs(delta) / IndentSensitive);
|
||||||
|
if (indent < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.indentStart = loc.event.globalX;
|
||||||
|
const direction = delta < 0 ? 'left' : 'right';
|
||||||
|
|
||||||
|
let parent = loc.target;
|
||||||
|
const index = loc.detail.index;
|
||||||
|
|
||||||
|
if (direction === 'left') {
|
||||||
|
if (!parent.parent || parent.isSlotRoot || index < parent.children.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [parent.parent, parent.index + 1];
|
||||||
|
} else {
|
||||||
|
if (index === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
parent = parent.children.get(index - 1) as any;
|
||||||
|
if (parent && parent.isContainer()) {
|
||||||
|
return [parent, parent.children.size];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* X 轴追踪器,左右移动光标时获取正确位置
|
|
||||||
*/
|
|
||||||
import { INode, INodeParent, isRootNode } from '../../../../document/node';
|
|
||||||
import Location, {
|
|
||||||
isLocationChildrenDetail,
|
|
||||||
LocationChildrenDetail,
|
|
||||||
LocationData,
|
|
||||||
LocationDetailType,
|
|
||||||
} from '../../../../document/location';
|
|
||||||
import { LocateEvent } from '../../../../globals';
|
|
||||||
import { isContainer } from './is-container';
|
|
||||||
|
|
||||||
export default class XAxisTracker {
|
|
||||||
private location!: Location;
|
|
||||||
private start: number = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param unit 移动单位
|
|
||||||
*/
|
|
||||||
constructor(readonly unit = 15) {}
|
|
||||||
|
|
||||||
track(loc: Location, e: LocateEvent): LocationData | null {
|
|
||||||
this.location = loc;
|
|
||||||
|
|
||||||
if (this.start === 0) {
|
|
||||||
this.start = e.globalX;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = this.locate(e);
|
|
||||||
|
|
||||||
if (!parent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
target: parent as INodeParent,
|
|
||||||
detail: {
|
|
||||||
type: LocationDetailType.Children,
|
|
||||||
index: parent.children.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定位
|
|
||||||
*/
|
|
||||||
locate(e: LocateEvent): INode | null {
|
|
||||||
if (!isLocationChildrenDetail(this.location.detail)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta = e.globalX - this.start;
|
|
||||||
let direction = null;
|
|
||||||
|
|
||||||
if (delta < 0) {
|
|
||||||
direction = 'left';
|
|
||||||
} else {
|
|
||||||
direction = 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = Math.floor(Math.abs(delta) / this.unit);
|
|
||||||
|
|
||||||
// console.log('x', e.globalX, 'y', e.globalY, 'delta', delta, 'n', n, 'start', this.start);
|
|
||||||
|
|
||||||
if (n < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一旦移动一个单位,就将"原点"清零
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
const node = this.location.target;
|
|
||||||
const index = (this.location.detail as LocationChildrenDetail).index;
|
|
||||||
let parent = null;
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
// 如果光标是往左运动
|
|
||||||
// 该节点如果不是最后一个节点,那么就没有继续查找下去的必要
|
|
||||||
// console.log('>>> [left]', index, node.children.length, node);
|
|
||||||
if (isRootNode(node)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// index 为 0 表示第一个位置
|
|
||||||
// 第一个位置或者不是最后以为位置,都不需要处理
|
|
||||||
if (index < node.children.length - 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
parent = node.parent as INode;
|
|
||||||
} else {
|
|
||||||
// 插入线一般是在元素下面,所以这边需要多减去 1,即 -2
|
|
||||||
if (index === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const i2 = Math.max(index - 1, 0);
|
|
||||||
parent = node.children[i2];
|
|
||||||
// console.log('>>> [right]', index, i2, parent, node.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// parent 节点判断
|
|
||||||
if (!parent || !isContainer(parent)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.start = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +1,63 @@
|
|||||||
import { computed, obx } from '../../globals';
|
import { computed, obx, uniqueId } from '../../globals';
|
||||||
import Designer from '../../designer/src/designer/designer';
|
import Designer from '../../designer/src/designer/designer';
|
||||||
import { ISensor, LocateEvent } from '../../designer/src/designer/helper/dragon';
|
import {
|
||||||
|
ISensor,
|
||||||
|
LocateEvent,
|
||||||
|
isDragNodeObject,
|
||||||
|
isDragAnyObject,
|
||||||
|
DragObject,
|
||||||
|
} from '../../designer/src/designer/helper/dragon';
|
||||||
|
import Scroller, { ScrollTarget, IScrollable } from '../../designer/src/designer/helper/scroller';
|
||||||
import { Tree } from './tree';
|
import { Tree } from './tree';
|
||||||
import Location from '../../designer/src/designer/helper/location';
|
import DropLocation, {
|
||||||
|
isLocationChildrenDetail,
|
||||||
|
LocationChildrenDetail,
|
||||||
|
LocationDetailType,
|
||||||
|
} from '../../designer/src/designer/helper/location';
|
||||||
|
import TreeNode from './tree-node';
|
||||||
|
import Node, { NodeParent, contains } from '../../designer/src/designer/document/node/node';
|
||||||
|
import { IndentTrack } from './helper/indent-track';
|
||||||
|
import DwellTimer from './helper/dwell-timer';
|
||||||
|
|
||||||
|
export interface IScrollBoard {
|
||||||
|
scrollToNode(treeNode: TreeNode, detail?: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
class TreeMaster {
|
class TreeMaster {
|
||||||
constructor(readonly designer: Designer) {
|
constructor(readonly designer: Designer) {
|
||||||
designer.dragon.onDragstart((e) => {
|
designer.dragon.onDragstart(e => {
|
||||||
const tree = this.currentTree;
|
const tree = this.currentTree;
|
||||||
if (tree) {
|
if (tree) {
|
||||||
tree.document.selection.getTopNodes().forEach(node => {
|
tree.document.selection.getTopNodes().forEach(node => {
|
||||||
tree.getTreeNode(node).setExpanded(false);
|
tree.getTreeNode(node).setExpanded(false);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
designer.activeTracker.onChange((target) => {
|
|
||||||
const tree = this.currentTree;
|
|
||||||
if (tree && target.node.document === tree.document) {
|
|
||||||
tree.getTreeNode(target.node).expandParents();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
designer.activeTracker.onChange(({ node, detail }) => {
|
||||||
|
const tree = this.currentTree;
|
||||||
|
if (!tree || node.document !== tree.document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeNode = tree.getTreeNode(node);
|
||||||
|
if (detail && isLocationChildrenDetail(detail)) {
|
||||||
|
treeNode.expand(true);
|
||||||
|
} else {
|
||||||
|
treeNode.expandParents();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.boards.forEach(board => {
|
||||||
|
board.scrollToNode(treeNode, detail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boards = new Set<IScrollBoard>();
|
||||||
|
addBoard(board: IScrollBoard) {
|
||||||
|
this.boards.add(board);
|
||||||
|
}
|
||||||
|
removeBoard(board: IScrollBoard) {
|
||||||
|
this.boards.delete(board);
|
||||||
}
|
}
|
||||||
|
|
||||||
private treeMap = new Map<string, Tree>();
|
private treeMap = new Map<string, Tree>();
|
||||||
@ -49,12 +87,13 @@ function getTreeMaster(designer: Designer): TreeMaster {
|
|||||||
return master;
|
return master;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OutlineMain implements ISensor {
|
export class OutlineMain implements ISensor, IScrollBoard, IScrollable {
|
||||||
private _designer?: Designer;
|
private _designer?: Designer;
|
||||||
@obx.ref private _master?: TreeMaster;
|
@obx.ref private _master?: TreeMaster;
|
||||||
get master() {
|
get master() {
|
||||||
return this._master;
|
return this._master;
|
||||||
}
|
}
|
||||||
|
readonly id = uniqueId('tree');
|
||||||
|
|
||||||
constructor(readonly editor: any) {
|
constructor(readonly editor: any) {
|
||||||
if (editor.designer) {
|
if (editor.designer) {
|
||||||
@ -66,30 +105,467 @@ export class OutlineMain implements ISensor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see ISensor
|
||||||
|
*/
|
||||||
fixEvent(e: LocateEvent): LocateEvent {
|
fixEvent(e: LocateEvent): LocateEvent {
|
||||||
throw new Error("Method not implemented.");
|
if (e.fixed) {
|
||||||
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
locate(e: LocateEvent): Location | undefined {
|
const notMyEvent = e.originalEvent.view?.document !== document;
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
|
if (!e.target || notMyEvent) {
|
||||||
|
e.target = document.elementFromPoint(e.canvasX!, e.canvasY!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// documentModel : 目标文档
|
||||||
|
e.documentModel = this._designer?.currentDocument;
|
||||||
|
|
||||||
|
// 事件已订正
|
||||||
|
e.fixed = true;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private indentTrack = new IndentTrack();
|
||||||
|
private dwell = new DwellTimer((target, event) => {
|
||||||
|
const document = target.document;
|
||||||
|
const designer = document.designer;
|
||||||
|
let index: any;
|
||||||
|
let focus: any;
|
||||||
|
let valid = true;
|
||||||
|
if (target.isSlotContainer()) {
|
||||||
|
index = null;
|
||||||
|
focus = { type: 'slots' };
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
valid = document.checkNesting(target, event.dragObject as any);
|
||||||
|
}
|
||||||
|
designer.createLocation({
|
||||||
|
target,
|
||||||
|
source: this.id,
|
||||||
|
event,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index,
|
||||||
|
focus,
|
||||||
|
valid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* @see ISensor
|
||||||
|
*/
|
||||||
|
locate(e: LocateEvent): DropLocation | undefined | null {
|
||||||
|
this.sensing = true;
|
||||||
|
this.scroller?.scrolling(e);
|
||||||
|
|
||||||
|
const tree = this._master?.currentTree;
|
||||||
|
if (!tree || !this._shell) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = tree.document;
|
||||||
|
const designer = document.designer;
|
||||||
|
const { globalY, dragObject } = e;
|
||||||
|
const pos = getPosFromEvent(e, this._shell);
|
||||||
|
const irect = this.getInsertionRect();
|
||||||
|
const originLoc = document.dropLocation;
|
||||||
|
if (originLoc && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom))) {
|
||||||
|
const loc = originLoc.clone(e);
|
||||||
|
const indented = this.indentTrack.getIndentParent(originLoc, loc);
|
||||||
|
if (indented) {
|
||||||
|
const [parent, index] = indented;
|
||||||
|
if (checkRecursion(parent, dragObject)) {
|
||||||
|
if (tree.getTreeNode(parent).expanded) {
|
||||||
|
this.dwell.reset();
|
||||||
|
return designer.createLocation({
|
||||||
|
target: parent,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index,
|
||||||
|
valid: document.checkNesting(parent, e.dragObject as any),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(originLoc.detail as LocationChildrenDetail).focus = {
|
||||||
|
type: 'node',
|
||||||
|
node: parent,
|
||||||
|
};
|
||||||
|
// focus try expand go on
|
||||||
|
this.dwell.focus(parent, e);
|
||||||
|
} else {
|
||||||
|
this.dwell.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// FIXME: recreate new location
|
||||||
|
if ((originLoc.detail as LocationChildrenDetail).near) {
|
||||||
|
(originLoc.detail as LocationChildrenDetail).near = undefined;
|
||||||
|
this.dwell.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.indentTrack.reset();
|
||||||
|
|
||||||
|
if (pos && pos !== 'unchanged') {
|
||||||
|
let treeNode = tree.getTreeNodeById(pos.nodeId);
|
||||||
|
if (treeNode) {
|
||||||
|
let focusSlots = pos.focusSlots;
|
||||||
|
let { node } = treeNode;
|
||||||
|
if (isDragNodeObject(dragObject)) {
|
||||||
|
const nodes = dragObject.nodes;
|
||||||
|
let i = nodes.length;
|
||||||
|
let p: any = node;
|
||||||
|
while (i-- > 0) {
|
||||||
|
if (contains(nodes[i], p)) {
|
||||||
|
p = nodes[i].parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p !== node) {
|
||||||
|
node = p || document.rootNode;
|
||||||
|
treeNode = tree.getTreeNode(node);
|
||||||
|
focusSlots = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusSlots) {
|
||||||
|
this.dwell.reset();
|
||||||
|
return designer.createLocation({
|
||||||
|
target: node as NodeParent,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index: null,
|
||||||
|
valid: false,
|
||||||
|
focus: { type: 'slots' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!treeNode.isRoot()) {
|
||||||
|
const loc = this.getNear(treeNode, e);
|
||||||
|
this.dwell.tryFocus(loc);
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loc = this.drillLocate(tree.root, e);
|
||||||
|
this.dwell.tryFocus(loc);
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNear(treeNode: TreeNode, e: LocateEvent, index?: number, rect?: DOMRect) {
|
||||||
|
const document = treeNode.tree.document;
|
||||||
|
const designer = document.designer;
|
||||||
|
const { globalY, dragObject } = e;
|
||||||
|
// TODO: check dragObject is anyData
|
||||||
|
const { node, expanded } = treeNode;
|
||||||
|
if (!rect) {
|
||||||
|
rect = this.getTreeNodeRect(treeNode);
|
||||||
|
if (!rect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index == null) {
|
||||||
|
index = node.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.isSlotRoot) {
|
||||||
|
// 是个插槽根节点
|
||||||
|
if (!treeNode.isContainer() && !treeNode.isSlotContainer()) {
|
||||||
|
return designer.createLocation({
|
||||||
|
target: node.parent!,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index: null,
|
||||||
|
near: { node, pos: 'replace' },
|
||||||
|
valid: true, // TODO: future validation the slot limit
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const loc1 = this.drillLocate(treeNode, e);
|
||||||
|
if (loc1) {
|
||||||
|
return loc1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return designer.createLocation({
|
||||||
|
target: node.parent!,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index: null,
|
||||||
|
valid: false,
|
||||||
|
focus: { type: 'slots' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let focusNode: Node | undefined;
|
||||||
|
// focus
|
||||||
|
if (!expanded && (treeNode.isContainer() || treeNode.isSlotContainer())) {
|
||||||
|
focusNode = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// before
|
||||||
|
const titleRect = this.getTreeTitleRect(treeNode) || rect;
|
||||||
|
if (globalY < titleRect.top + titleRect.height / 2) {
|
||||||
|
return designer.createLocation({
|
||||||
|
target: node.parent!,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index,
|
||||||
|
valid: document.checkNesting(node.parent!, dragObject as any),
|
||||||
|
near: { node, pos: 'before' },
|
||||||
|
focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalY > titleRect.bottom) {
|
||||||
|
focusNode = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
// drill
|
||||||
|
const loc = this.drillLocate(treeNode, e);
|
||||||
|
if (loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// after
|
||||||
|
return designer.createLocation({
|
||||||
|
target: node.parent!,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
detail: {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
index: index + 1,
|
||||||
|
valid: document.checkNesting(node.parent!, dragObject as any),
|
||||||
|
near: { node, pos: 'after' },
|
||||||
|
focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private drillLocate(treeNode: TreeNode, e: LocateEvent): DropLocation | null {
|
||||||
|
const document = treeNode.tree.document;
|
||||||
|
const designer = document.designer;
|
||||||
|
const { dragObject, globalY } = e;
|
||||||
|
|
||||||
|
if (!checkRecursion(treeNode.node, dragObject)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDragAnyObject(dragObject)) {
|
||||||
|
// TODO: future
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = treeNode.node as NodeParent;
|
||||||
|
const detail: LocationChildrenDetail = {
|
||||||
|
type: LocationDetailType.Children,
|
||||||
|
};
|
||||||
|
const locationData: any = {
|
||||||
|
target: container,
|
||||||
|
detail,
|
||||||
|
source: this.id,
|
||||||
|
event: e,
|
||||||
|
};
|
||||||
|
const isSlotContainer = treeNode.isSlotContainer();
|
||||||
|
const isContainer = treeNode.isContainer();
|
||||||
|
|
||||||
|
if (container.isSlotRoot && !treeNode.expanded) {
|
||||||
|
// 未展开,直接定位到内部第一个节点
|
||||||
|
if (isSlotContainer) {
|
||||||
|
detail.index = null;
|
||||||
|
detail.focus = { type: 'slots' };
|
||||||
|
detail.valid = false;
|
||||||
|
} else {
|
||||||
|
detail.index = 0;
|
||||||
|
detail.valid = document.checkNesting(container, dragObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: TreeNode[] | null = null;
|
||||||
|
let slotsRect: DOMRect | undefined;
|
||||||
|
let focusSlots: boolean = false;
|
||||||
|
// isSlotContainer
|
||||||
|
if (isSlotContainer) {
|
||||||
|
slotsRect = this.getTreeSlotsRect(treeNode);
|
||||||
|
if (slotsRect) {
|
||||||
|
if (globalY <= slotsRect.bottom) {
|
||||||
|
focusSlots = true;
|
||||||
|
items = treeNode.slots;
|
||||||
|
} else if (!isContainer) {
|
||||||
|
// 不在 slots 范围,又不是 container 的情况,高亮 slots 区
|
||||||
|
detail.index = null;
|
||||||
|
detail.focus = { type: 'slots' };
|
||||||
|
detail.valid = false;
|
||||||
|
return designer.createLocation(locationData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items && isContainer) {
|
||||||
|
items = treeNode.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = items.length;
|
||||||
|
let index = 0;
|
||||||
|
let before = l < 1;
|
||||||
|
let current: TreeNode | undefined;
|
||||||
|
let currentIndex = index;
|
||||||
|
for (; index < l; index++) {
|
||||||
|
current = items[index];
|
||||||
|
currentIndex = index;
|
||||||
|
const rect = this.getTreeNodeRect(current);
|
||||||
|
if (!rect) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rect
|
||||||
|
if (globalY < rect.top) {
|
||||||
|
before = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalY > rect.bottom) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loc = this.getNear(current, e, index, rect);
|
||||||
|
if (loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusSlots) {
|
||||||
|
detail.focus = { type: 'slots' };
|
||||||
|
detail.valid = false;
|
||||||
|
detail.index = null;
|
||||||
|
} else {
|
||||||
|
if (current) {
|
||||||
|
detail.index = before ? currentIndex : currentIndex + 1;
|
||||||
|
detail.near = { node: current.node, pos: before ? 'before' : 'after' };
|
||||||
|
} else {
|
||||||
|
detail.index = l;
|
||||||
|
}
|
||||||
|
detail.valid = document.checkNesting(container, dragObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return designer.createLocation(locationData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see ISensor
|
||||||
|
*/
|
||||||
isEnter(e: LocateEvent): boolean {
|
isEnter(e: LocateEvent): boolean {
|
||||||
throw new Error("Method not implemented.");
|
if (!this._shell) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rect = this._shell.getBoundingClientRect();
|
||||||
|
return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
deactiveSensor(): void {
|
private tryScrollAgain: number | null = null;
|
||||||
throw new Error("Method not implemented.");
|
/**
|
||||||
|
* @see IScrollBoard
|
||||||
|
*/
|
||||||
|
scrollToNode(treeNode: TreeNode, detail?: any, tryTimes: number = 0) {
|
||||||
|
this.tryScrollAgain = null;
|
||||||
|
if (this.sensing || !this.bounds || !this.scroller || !this.scrollTarget) {
|
||||||
|
// is a active sensor
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const opt: any = {};
|
||||||
|
let scroll = false;
|
||||||
|
let rect: ClientRect | undefined;
|
||||||
|
if (detail && isLocationChildrenDetail(detail)) {
|
||||||
|
rect = this.getInsertionRect();
|
||||||
|
} else {
|
||||||
|
rect = this.getTreeNodeRect(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
if (!this.tryScrollAgain && tryTimes < 3) {
|
||||||
|
this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(treeNode, detail, tryTimes + 1));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scrollTarget = this.scrollTarget;
|
||||||
|
const st = scrollTarget.top;
|
||||||
|
const scrollHeight = scrollTarget.scrollHeight;
|
||||||
|
const { height, top, bottom } = this.bounds;
|
||||||
|
|
||||||
|
if (rect.top < top || rect.bottom > bottom) {
|
||||||
|
opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height);
|
||||||
|
scroll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
this.scroller.scrollTo(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sensing = false;
|
||||||
|
/**
|
||||||
|
* @see ISensor
|
||||||
|
*/
|
||||||
|
deactiveSensor() {
|
||||||
|
this.sensing = false;
|
||||||
|
this.scroller?.cancel();
|
||||||
|
this.dwell.reset();
|
||||||
|
this.indentTrack.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see IScrollable
|
||||||
|
*/
|
||||||
|
get bounds(): DOMRect | null {
|
||||||
|
if (!this._shell) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this._shell.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scrollTarget?: ScrollTarget;
|
||||||
|
/**
|
||||||
|
* @see IScrollable
|
||||||
|
*/
|
||||||
|
get scrollTarget() {
|
||||||
|
return this._scrollTarget;
|
||||||
|
}
|
||||||
|
private scroller?: Scroller;
|
||||||
|
|
||||||
private setupDesigner(designer: Designer) {
|
private setupDesigner(designer: Designer) {
|
||||||
this._designer = designer;
|
this._designer = designer;
|
||||||
this._master = getTreeMaster(designer);
|
this._master = getTreeMaster(designer);
|
||||||
// designer.dragon.addSensor(this);
|
this._master.addBoard(this);
|
||||||
|
designer.dragon.addSensor(this);
|
||||||
|
this.scroller = designer.createScroller(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
purge() {
|
purge() {
|
||||||
this._designer?.dragon.removeSensor(this);
|
this._designer?.dragon.removeSensor(this);
|
||||||
|
this._master?.removeBoard(this);
|
||||||
// todo purge treeMaster if needed
|
// todo purge treeMaster if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,9 +584,80 @@ export class OutlineMain implements ISensor {
|
|||||||
}
|
}
|
||||||
this._shell = shell;
|
this._shell = shell;
|
||||||
if (shell) {
|
if (shell) {
|
||||||
// this._sensorAvailable = true;
|
this._scrollTarget = new ScrollTarget(shell);
|
||||||
|
this._sensorAvailable = true;
|
||||||
|
} else {
|
||||||
|
this._scrollTarget = undefined;
|
||||||
|
this._sensorAvailable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getInsertionRect(): DOMRect | undefined {
|
||||||
|
if (!this._shell) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this._shell.querySelector('.insertion')?.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTreeNodeRect(treeNode: TreeNode): DOMRect | undefined {
|
||||||
|
if (!this._shell) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this._shell.querySelector(`.tree-node[data-id="${treeNode.id}"]`)?.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTreeTitleRect(treeNode: TreeNode): DOMRect | undefined {
|
||||||
|
if (!this._shell) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this._shell.querySelector(`.tree-node-title[data-id="${treeNode.id}"]`)?.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTreeSlotsRect(treeNode: TreeNode): DOMRect | undefined {
|
||||||
|
if (!this._shell) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this._shell.querySelector(`.tree-node-slots[data-id="${treeNode.id}"]`)?.getBoundingClientRect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkRecursion(parent: Node | undefined | null, dragObject: DragObject): parent is NodeParent {
|
||||||
|
if (!parent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isDragNodeObject(dragObject)) {
|
||||||
|
const nodes = dragObject.nodes;
|
||||||
|
if (nodes.some(node => node.contains(parent))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPosFromEvent(
|
||||||
|
{ target }: LocateEvent,
|
||||||
|
stop: Element,
|
||||||
|
):
|
||||||
|
| null
|
||||||
|
| 'unchanged'
|
||||||
|
| {
|
||||||
|
nodeId: string;
|
||||||
|
focusSlots: boolean;
|
||||||
|
} {
|
||||||
|
if (!target || !stop.contains(target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (target.matches('.insertion')) {
|
||||||
|
return 'unchanged';
|
||||||
|
}
|
||||||
|
target = target.closest('[data-id]');
|
||||||
|
if (!target || !stop.contains(target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeId = (target as HTMLDivElement).dataset.id!;
|
||||||
|
return {
|
||||||
|
focusSlots: target.matches('.tree-node-slots'),
|
||||||
|
nodeId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -1,220 +0,0 @@
|
|||||||
import { ISenseAble, LocateEvent, isNodesDragTarget, activeTracker, getCurrentDocument } from '../../../globals';
|
|
||||||
import Location, { isLocationChildrenDetail, LocationDetailType } from '../../../document/location';
|
|
||||||
import tree from './tree';
|
|
||||||
import Scroller, { ScrollTarget } from '../../../document/scroller';
|
|
||||||
import { isShadowNode } from '../../../document/node/shadow-node';
|
|
||||||
import TreeNode from './tree-node';
|
|
||||||
import { INodeParent } from '../../../document/node';
|
|
||||||
import DwellTimer from './helper/dwell-timer';
|
|
||||||
import XAxisTracker from './helper/x-axis-tracker';
|
|
||||||
|
|
||||||
export const OutlineBoardID = 'outline-board';
|
|
||||||
export default class OutlineBoard implements ISenseAble {
|
|
||||||
id = OutlineBoardID;
|
|
||||||
|
|
||||||
get bounds() {
|
|
||||||
const rootElement = this.element;
|
|
||||||
const clientBound = rootElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
height: clientBound.height,
|
|
||||||
width: clientBound.width,
|
|
||||||
top: clientBound.top,
|
|
||||||
left: clientBound.left,
|
|
||||||
right: clientBound.right,
|
|
||||||
bottom: clientBound.bottom,
|
|
||||||
scale: 1,
|
|
||||||
scrollHeight: rootElement.scrollHeight,
|
|
||||||
scrollWidth: rootElement.scrollWidth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
sensitive: boolean = true;
|
|
||||||
private sensing: boolean = false;
|
|
||||||
|
|
||||||
private scrollTarget = new ScrollTarget(this.element);
|
|
||||||
private scroller = new Scroller(this, this.scrollTarget);
|
|
||||||
|
|
||||||
constructor(readonly element: HTMLDivElement) {
|
|
||||||
activeTracker.onChange(({ node, detail }) => {
|
|
||||||
const treeNode = isShadowNode(node) ? tree.getTreeNode(node.origin) : tree.getTreeNode(node);
|
|
||||||
if (treeNode.hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detail && detail.type === LocationDetailType.Children) {
|
|
||||||
treeNode.expand(true);
|
|
||||||
} else {
|
|
||||||
treeNode.expandParents();
|
|
||||||
}
|
|
||||||
this.scrollToNode(treeNode, detail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private tryScrollAgain: number | null = null;
|
|
||||||
scrollToNode(treeNode: TreeNode, detail?: any, tryTimes: number = 0) {
|
|
||||||
this.tryScrollAgain = null;
|
|
||||||
if (this.sensing) {
|
|
||||||
// is a active sensor
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const opt: any = {};
|
|
||||||
let scroll = false;
|
|
||||||
let rect: ClientRect | null;
|
|
||||||
if (detail && isLocationChildrenDetail(detail)) {
|
|
||||||
rect = tree.getInsertionRect();
|
|
||||||
} else {
|
|
||||||
rect = treeNode.computeRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rect) {
|
|
||||||
if (!this.tryScrollAgain && tryTimes < 3) {
|
|
||||||
this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(treeNode, detail, tryTimes + 1));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const scrollTarget = this.scrollTarget;
|
|
||||||
const st = scrollTarget.top;
|
|
||||||
const { height, top, bottom, scrollHeight } = this.bounds;
|
|
||||||
|
|
||||||
if (rect.top < top || rect.bottom > bottom) {
|
|
||||||
opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height);
|
|
||||||
scroll = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scroll && this.scroller) {
|
|
||||||
this.scroller.scrollTo(opt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnter(e: LocateEvent): boolean {
|
|
||||||
return this.inRange(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
inRange(e: LocateEvent): boolean {
|
|
||||||
const rect = this.bounds;
|
|
||||||
return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
deactive(): void {
|
|
||||||
this.sensing = false;
|
|
||||||
console.log('>>> deactive');
|
|
||||||
}
|
|
||||||
|
|
||||||
fixEvent(e: LocateEvent): LocateEvent {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private dwellTimer: DwellTimer = new DwellTimer(450);
|
|
||||||
private xAxisTracker = new XAxisTracker();
|
|
||||||
|
|
||||||
locate(e: LocateEvent): Location | undefined {
|
|
||||||
this.sensing = true;
|
|
||||||
this.scroller.scrolling(e);
|
|
||||||
|
|
||||||
const dragTarget = e.dragTarget;
|
|
||||||
// FIXME: not support multiples/nodedatas/any data,
|
|
||||||
const dragment = isNodesDragTarget(dragTarget) ? dragTarget.nodes[0] : null;
|
|
||||||
if (!dragment) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const doc = getCurrentDocument()!;
|
|
||||||
const preDraggedNode = doc.dropLocation && doc.dropLocation.target;
|
|
||||||
|
|
||||||
// 左右移动追踪,一旦左右移动满足位置条件,直接返回即可。
|
|
||||||
if (doc.dropLocation) {
|
|
||||||
const loc2 = this.xAxisTracker.track(doc.dropLocation, e);
|
|
||||||
if (loc2) {
|
|
||||||
this.dwellTimer.end();
|
|
||||||
return doc.createLocation(loc2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.dwellTimer.end();
|
|
||||||
return doc.createLocation({
|
|
||||||
target: dragment.parent!,
|
|
||||||
detail: {
|
|
||||||
type: LocationDetailType.Children,
|
|
||||||
index: dragment.index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这语句的后半段是解决"丢帧"问题
|
|
||||||
// e 有一种情况,是从 root > .flow 开始冒泡,而不是实际节点。这种情况往往发生在:光标在插入框内移动
|
|
||||||
// 此时取上一次插入位置的 node 即可
|
|
||||||
const treeNode = tree.getTreeNodeByEvent(e as any) || (preDraggedNode && tree.getTreeNode(preDraggedNode));
|
|
||||||
|
|
||||||
// TODO: 没有判断是否可以放入 isDropContainer,决定 target 的值是父节点还是本节点
|
|
||||||
if (!treeNode || dragment === treeNode.node || treeNode.ignored) {
|
|
||||||
this.dwellTimer.end();
|
|
||||||
console.warn('not found tree-node or other reasons', treeNode, e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('I am at', treeNode.id, e);
|
|
||||||
|
|
||||||
const rect = treeNode.computeRect();
|
|
||||||
if (!rect) {
|
|
||||||
this.dwellTimer.end();
|
|
||||||
console.warn('can not get the rect, node', treeNode.id);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = treeNode.node;
|
|
||||||
const parentNode = node.parent;
|
|
||||||
|
|
||||||
if (!parentNode) {
|
|
||||||
this.dwellTimer.end();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = Math.max(parentNode.children.indexOf(node), 0);
|
|
||||||
const center = rect.top + rect.height / 2;
|
|
||||||
|
|
||||||
// 常规处理
|
|
||||||
// 如果可以展开,但是没有展开,需要设置延时器,检查停留时间然后展开
|
|
||||||
// 最后返回合适的位置信息
|
|
||||||
// FIXME: 容器判断存在问题,比如 img 是可以被放入的
|
|
||||||
if (treeNode.isContainer() && !treeNode.expanded) {
|
|
||||||
if (e.globalY > center) {
|
|
||||||
this.dwellTimer.start(treeNode.id, () => {
|
|
||||||
doc.createLocation({
|
|
||||||
target: node as INodeParent,
|
|
||||||
detail: {
|
|
||||||
type: LocationDetailType.Children,
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.dwellTimer.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果节点是展开状态,并且光标是在其下方,不做任何处理,直接返回即可
|
|
||||||
// 如果不做这个处理,那么会出现"抖动"情况:在当前元素中心线下方时,会作为该元素的第一个子节点插入,而又会碰到已经存在对第一个字节点"争相"处理
|
|
||||||
if (treeNode.expanded) {
|
|
||||||
if (e.globalY > center) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果光标移动到节点中心线下方,则将元素插入到该节点下方
|
|
||||||
// 反之插入该节点上方
|
|
||||||
if (e.globalY > center) {
|
|
||||||
// down
|
|
||||||
index = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
index = Math.min(index, parentNode.children.length);
|
|
||||||
|
|
||||||
return doc.createLocation({
|
|
||||||
target: parentNode,
|
|
||||||
detail: {
|
|
||||||
type: LocationDetailType.Children,
|
|
||||||
index,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { computed, obx, TitleContent, isI18nData, localeFormat } from '../../globals';
|
import { computed, obx, TitleContent, isI18nData, localeFormat } from '../../globals';
|
||||||
import Node from '../../designer/src/designer/document/node/node';
|
import Node from '../../designer/src/designer/document/node/node';
|
||||||
import DocumentModel from '../../designer/src/designer/document/document-model';
|
import DocumentModel from '../../designer/src/designer/document/document-model';
|
||||||
import { isLocationChildrenDetail } from '../../designer/src/designer/helper/location';
|
import { isLocationChildrenDetail, LocationChildrenDetail } from '../../designer/src/designer/helper/location';
|
||||||
import Designer from '../../designer/src/designer/designer';
|
import Designer from '../../designer/src/designer/designer';
|
||||||
import { Tree } from './tree';
|
import { Tree } from './tree';
|
||||||
|
|
||||||
@ -14,15 +14,15 @@ export default class TreeNode {
|
|||||||
* 是否可以展开
|
* 是否可以展开
|
||||||
*/
|
*/
|
||||||
@computed get expandable(): boolean {
|
@computed get expandable(): boolean {
|
||||||
return this.hasChildren() || this.isSlotContainer() || this.dropIndex != null;
|
return this.hasChildren() || this.isSlotContainer() || this.dropDetail?.index != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插入"线"位置信息
|
* 插入"线"位置信息
|
||||||
*/
|
*/
|
||||||
@computed get dropIndex(): number | null {
|
@computed get dropDetail(): LocationChildrenDetail | undefined | null {
|
||||||
const loc = this.node.document.dropLocation;
|
const loc = this.node.document.dropLocation;
|
||||||
return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail.index : null;
|
return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get depth(): number {
|
@computed get depth(): number {
|
||||||
@ -44,6 +44,14 @@ export default class TreeNode {
|
|||||||
return loc.target === this.node;
|
return loc.target === this.node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed isFocusingNode(): boolean {
|
||||||
|
const loc = this.node.document.dropLocation;
|
||||||
|
if (!loc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isLocationChildrenDetail(loc.detail) && loc.detail.focus?.type === 'node' && loc.detail.focus.node === this.node;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认为折叠状态
|
* 默认为折叠状态
|
||||||
* 在初始化根节点时,设置为展开状态
|
* 在初始化根节点时,设置为展开状态
|
||||||
@ -198,65 +206,6 @@ export default class TreeNode {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* 展开节点,支持依次展开父节点
|
|
||||||
*/
|
|
||||||
expand(tryExpandParents: boolean = false) {
|
|
||||||
// 这边不能直接使用 expanded,需要额外判断是否可以展开
|
|
||||||
// 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开
|
|
||||||
if (this.expandable && !this._expanded) {
|
|
||||||
this.setExpanded(true);
|
|
||||||
}
|
|
||||||
if (tryExpandParents) {
|
|
||||||
this.expandParents();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 光标停留处理
|
|
||||||
* 超过一定时间,展开节点
|
|
||||||
*/
|
|
||||||
private dwellTimer: number | undefined;
|
|
||||||
clearDwellTimer() {
|
|
||||||
clearTimeout(this.dwellTimer);
|
|
||||||
this.dwellTimer = undefined;
|
|
||||||
}
|
|
||||||
willExpand() {
|
|
||||||
if (this.dwellTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.clearDwellTimer();
|
|
||||||
if (this.expanded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.dwellTimer = setTimeout(() => {
|
|
||||||
this.clearDwellTimer();
|
|
||||||
this.expand(true);
|
|
||||||
}, 400) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
expandParents() {
|
|
||||||
let p = this.node.parent;
|
|
||||||
while (p) {
|
|
||||||
this.tree.getTreeNode(p).setExpanded(true);
|
|
||||||
p = p.parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private titleRef: HTMLDivElement | null = null;
|
|
||||||
mount(ref: HTMLDivElement | null) {
|
|
||||||
this.titleRef = ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
computeRect() {
|
|
||||||
let target = this.titleRef;
|
|
||||||
if (!target) {
|
|
||||||
const nodeId = this.id;
|
|
||||||
target = window.document.querySelector(`div[data-id="${nodeId}"]`);
|
|
||||||
}
|
|
||||||
return target && target.getBoundingClientRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
select(isMulti: boolean) {
|
select(isMulti: boolean) {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
|
|
||||||
@ -274,6 +223,28 @@ export default class TreeNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展开节点,支持依次展开父节点
|
||||||
|
*/
|
||||||
|
expand(tryExpandParents: boolean = false) {
|
||||||
|
// 这边不能直接使用 expanded,需要额外判断是否可以展开
|
||||||
|
// 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开
|
||||||
|
if (this.expandable && !this._expanded) {
|
||||||
|
this.setExpanded(true);
|
||||||
|
}
|
||||||
|
if (tryExpandParents) {
|
||||||
|
this.expandParents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expandParents() {
|
||||||
|
let p = this.node.parent;
|
||||||
|
while (p) {
|
||||||
|
this.tree.getTreeNode(p).setExpanded(true);
|
||||||
|
p = p.parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readonly designer: Designer;
|
readonly designer: Designer;
|
||||||
readonly document: DocumentModel;
|
readonly document: DocumentModel;
|
||||||
@obx.ref private _node: Node;
|
@obx.ref private _node: Node;
|
||||||
|
|||||||
@ -25,4 +25,8 @@ export class Tree {
|
|||||||
this.treeNodesMap.set(node.id, treeNode);
|
this.treeNodesMap.set(node.id, treeNode);
|
||||||
return treeNode;
|
return treeNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTreeNodeById(id: string) {
|
||||||
|
return this.treeNodesMap.get(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.lc-outline-tree {
|
.lc-outline-tree {
|
||||||
|
@treeNodeHeight: 30px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 20px;
|
margin-bottom: @treeNodeHeight;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.tree-node-branches::before {
|
.tree-node-branches::before {
|
||||||
@ -28,6 +29,7 @@
|
|||||||
left: 6px;
|
left: 6px;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -37,10 +39,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.insertion {
|
.insertion {
|
||||||
pointer-events: none !important;
|
pointer-events: all !important;
|
||||||
border: 1px dashed var(--color-brand-light);
|
border: 1px dashed var(--color-brand-light);
|
||||||
height: 18px;
|
height: @treeNodeHeight;
|
||||||
|
box-sizing: border-box;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
|
&.invalid {
|
||||||
|
border-color: red;
|
||||||
|
background-color: rgba(240, 154, 154, 0.719);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition-group-container {
|
.condition-group-container {
|
||||||
@ -75,7 +82,7 @@
|
|||||||
.tree-node-slots {
|
.tree-node-slots {
|
||||||
border-bottom: 1px solid rgb(144, 94, 190);
|
border-bottom: 1px solid rgb(144, 94, 190);
|
||||||
position: relative;
|
position: relative;
|
||||||
&:before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
width: 0;
|
width: 0;
|
||||||
@ -99,6 +106,16 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.insertion-at-slots {
|
||||||
|
padding-bottom: @treeNodeHeight;
|
||||||
|
border-bottom-color: rgb(182, 55, 55);
|
||||||
|
>.tree-node-slots-title {
|
||||||
|
background-color: rgb(182, 55, 55);
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
border-left-color: rgb(182, 55, 55);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node {
|
.tree-node {
|
||||||
@ -146,7 +163,8 @@
|
|||||||
border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
|
border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 30px;
|
height: @treeNodeHeight;
|
||||||
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
@ -196,6 +214,12 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
html.lc-cursor-dragging & {
|
||||||
|
// FIXME: only hide hover shows
|
||||||
|
.tree-node-hide-btn, .tree-node-lock-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
&.editing {
|
&.editing {
|
||||||
& > .tree-node-hide-btn, & >.tree-node-lock-btn {
|
& > .tree-node-hide-btn, & >.tree-node-lock-btn {
|
||||||
display: none;
|
display: none;
|
||||||
@ -285,6 +309,22 @@
|
|||||||
& > .tree-node-branches::before {
|
& > .tree-node-branches::before {
|
||||||
border-left: 1px solid var(--color-brand);
|
border-left: 1px solid var(--color-brand);
|
||||||
}
|
}
|
||||||
|
& > .tree-node-title {
|
||||||
|
.tree-node-expand-btn {
|
||||||
|
color: var(--color-brand);
|
||||||
|
}
|
||||||
|
.tree-node-icon {
|
||||||
|
color: var(--color-brand);
|
||||||
|
}
|
||||||
|
.tree-node-title-label > .lc-title {
|
||||||
|
color: var(--color-brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.highlight {
|
||||||
|
& > .tree-node-title {
|
||||||
|
background: var(--color-block-background-shallow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-branches {
|
.tree-node-branches {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { observer, Title } from '../../../globals';
|
import { observer, Title } from '../../../globals';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import TreeNode from '../tree-node';
|
import TreeNode from '../tree-node';
|
||||||
import TreeNodeView from './tree-node';
|
import TreeNodeView from './tree-node';
|
||||||
import ExclusiveGroup from '../../../designer/src/designer/document/node/exclusive-group';
|
import ExclusiveGroup from '../../../designer/src/designer/document/node/exclusive-group';
|
||||||
@ -55,7 +56,16 @@ class TreeNodeChildren extends Component<{
|
|||||||
groupContents = [];
|
groupContents = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { dropIndex } = treeNode;
|
const dropDetail = treeNode.dropDetail;
|
||||||
|
const dropIndex = dropDetail?.index;
|
||||||
|
const insertion = (
|
||||||
|
<div
|
||||||
|
key="insertion"
|
||||||
|
className={classNames('insertion', {
|
||||||
|
invalid: dropDetail?.valid === false,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
treeNode.children?.forEach((child, index) => {
|
treeNode.children?.forEach((child, index) => {
|
||||||
const { conditionGroup } = child.node;
|
const { conditionGroup } = child.node;
|
||||||
if (conditionGroup !== currentGrp) {
|
if (conditionGroup !== currentGrp) {
|
||||||
@ -66,22 +76,23 @@ class TreeNodeChildren extends Component<{
|
|||||||
currentGrp = conditionGroup;
|
currentGrp = conditionGroup;
|
||||||
if (index === dropIndex) {
|
if (index === dropIndex) {
|
||||||
if (groupContents.length > 0) {
|
if (groupContents.length > 0) {
|
||||||
groupContents.push(<div key="insertion" className="insertion" />);
|
groupContents.push(insertion);
|
||||||
} else {
|
} else {
|
||||||
children.push(<div key="insertion" className="insertion" />);
|
children.push(insertion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
groupContents.push(<TreeNodeView key={child.id} treeNode={child} />);
|
groupContents.push(<TreeNodeView key={child.id} treeNode={child} />);
|
||||||
} else {
|
} else {
|
||||||
if (index === dropIndex) {
|
if (index === dropIndex) {
|
||||||
children.push(<div key="insertion" className="insertion" />);
|
children.push(insertion);
|
||||||
}
|
}
|
||||||
children.push(<TreeNodeView key={child.id} treeNode={child} />);
|
children.push(<TreeNodeView key={child.id} treeNode={child} />);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
endGroup();
|
endGroup();
|
||||||
if (dropIndex != null && dropIndex === treeNode.children?.length) {
|
const length = treeNode.children?.length || 0;
|
||||||
children.push(<div key="insertion" className="insertion" />);
|
if (dropIndex != null && dropIndex >= length) {
|
||||||
|
children.push(insertion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="tree-node-children">{children}</div>;
|
return <div className="tree-node-children">{children}</div>;
|
||||||
@ -101,9 +112,14 @@ class TreeNodeSlots extends Component<{
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="tree-node-slots">
|
<div
|
||||||
|
className={classNames('tree-node-slots', {
|
||||||
|
'insertion-at-slots': treeNode.dropDetail?.focus?.type === 'slots',
|
||||||
|
})}
|
||||||
|
data-id={treeNode.id}
|
||||||
|
>
|
||||||
<div className="tree-node-slots-title">
|
<div className="tree-node-slots-title">
|
||||||
<Title title={{ type: 'i18n', intl: intl('Slots')}} />
|
<Title title={{ type: 'i18n', intl: intl('Slots') }} />
|
||||||
</div>
|
</div>
|
||||||
{treeNode.slots.map(tnode => (
|
{treeNode.slots.map(tnode => (
|
||||||
<TreeNodeView key={tnode.id} treeNode={tnode} />
|
<TreeNodeView key={tnode.id} treeNode={tnode} />
|
||||||
|
|||||||
@ -27,10 +27,10 @@ export default class TreeNodeView extends Component<{ treeNode: TreeNode }> {
|
|||||||
// 是否锁定的
|
// 是否锁定的
|
||||||
locked: treeNode.locked,
|
locked: treeNode.locked,
|
||||||
// 是否投放响应
|
// 是否投放响应
|
||||||
dropping: treeNode.dropIndex != null,
|
dropping: treeNode.dropDetail?.index != null,
|
||||||
'is-root': treeNode.isRoot(),
|
'is-root': treeNode.isRoot(),
|
||||||
'condition-flow': treeNode.node.conditionGroup != null,
|
'condition-flow': treeNode.node.conditionGroup != null,
|
||||||
// highlight: treeNode.isResponseDropping() && treeNode.dropIndex == null,
|
highlight: treeNode.isFocusingNode(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -61,7 +61,8 @@ export default class TreeTitle extends Component<{
|
|||||||
const { treeNode } = this.props;
|
const { treeNode } = this.props;
|
||||||
const { editing } = this.state;
|
const { editing } = this.state;
|
||||||
const isCNode = !treeNode.isRoot();
|
const isCNode = !treeNode.isRoot();
|
||||||
const isNodeParent = treeNode.node.isNodeParent;
|
const { node } = treeNode;
|
||||||
|
const isNodeParent = node.isNodeParent;
|
||||||
let style: any;
|
let style: any;
|
||||||
if (isCNode) {
|
if (isCNode) {
|
||||||
const depth = treeNode.depth;
|
const depth = treeNode.depth;
|
||||||
@ -77,9 +78,9 @@ export default class TreeTitle extends Component<{
|
|||||||
className={classNames('tree-node-title', {
|
className={classNames('tree-node-title', {
|
||||||
editing,
|
editing,
|
||||||
})}
|
})}
|
||||||
ref={ref => treeNode.mount(ref)}
|
|
||||||
style={style}
|
style={style}
|
||||||
onClick={treeNode.node.conditionGroup ? () => treeNode.node.setConditionalVisible() : undefined}
|
data-id={treeNode.id}
|
||||||
|
onClick={node.conditionGroup ? () => node.setConditionalVisible() : undefined}
|
||||||
>
|
>
|
||||||
{isCNode && <ExpandBtn treeNode={treeNode} />}
|
{isCNode && <ExpandBtn treeNode={treeNode} />}
|
||||||
<div className="tree-node-icon">{createIcon(treeNode.icon)}</div>
|
<div className="tree-node-icon">{createIcon(treeNode.icon)}</div>
|
||||||
@ -94,19 +95,19 @@ export default class TreeTitle extends Component<{
|
|||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Title title={treeNode.title} />
|
<Title title={treeNode.title} />
|
||||||
{treeNode.node.slotFor && (<a className="tree-node-tag slot">
|
{node.slotFor && (<a className="tree-node-tag slot">
|
||||||
{/* todo: click redirect to prop */}
|
{/* todo: click redirect to prop */}
|
||||||
<IconSlot />
|
<IconSlot />
|
||||||
<EmbedTip>{intl('Slot for {prop}', { prop: treeNode.node.slotFor.key })}</EmbedTip>
|
<EmbedTip>{intl('Slot for {prop}', { prop: node.slotFor.key })}</EmbedTip>
|
||||||
</a>)}
|
</a>)}
|
||||||
{treeNode.node.hasLoop() && (
|
{node.hasLoop() && (
|
||||||
<a className="tree-node-tag loop">
|
<a className="tree-node-tag loop">
|
||||||
{/* todo: click todo something */}
|
{/* todo: click todo something */}
|
||||||
<IconLoop />
|
<IconLoop />
|
||||||
<EmbedTip>{intl('Loop')}</EmbedTip>
|
<EmbedTip>{intl('Loop')}</EmbedTip>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{treeNode.node.hasCondition() && !treeNode.node.conditionGroup && (
|
{node.hasCondition() && !node.conditionGroup && (
|
||||||
<a className="tree-node-tag cond">
|
<a className="tree-node-tag cond">
|
||||||
{/* todo: click todo something */}
|
{/* todo: click todo something */}
|
||||||
<IconCond />
|
<IconCond />
|
||||||
|
|||||||
@ -1,83 +1,140 @@
|
|||||||
import { Component } from 'react';
|
import { Component, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import { observer } from '../../../globals';
|
import { observer } from '../../../globals';
|
||||||
import { Tree } from '../tree';
|
import { Tree } from '../tree';
|
||||||
import TreeNodeView from './tree-node';
|
import TreeNodeView from './tree-node';
|
||||||
|
import { isRootNode } from '../../../designer/src/designer/document/node/root-node';
|
||||||
|
import Node from '../../../designer/src/designer/document/node/node';
|
||||||
|
import { DragObjectType, isShaken } from '../../../designer/src/designer/helper/dragon';
|
||||||
|
|
||||||
|
function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string {
|
||||||
|
let target: Element | null = e.target as Element;
|
||||||
|
if (!target || !stop.contains(target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
target = target.closest('[data-id]');
|
||||||
|
if (!target || !stop.contains(target)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (target as HTMLDivElement).dataset.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class TreeView extends Component<{ tree: Tree }> {
|
export default class TreeView extends Component<{ tree: Tree }> {
|
||||||
/*
|
private shell: HTMLDivElement | null = null;
|
||||||
hover(e: any) {
|
private hover(e: ReactMouseEvent) {
|
||||||
const treeNode = tree.getTreeNodeByEvent(e);
|
const { tree } = this.props;
|
||||||
|
|
||||||
|
const doc = tree.document;
|
||||||
|
const hovering = doc.designer.hovering;
|
||||||
|
if (!hovering.enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = this.getTreeNodeFromEvent(e)?.node;
|
||||||
|
hovering.hover(node || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClick = (e: ReactMouseEvent) => {
|
||||||
|
if (this.ignoreUpSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.boostEvent && isShaken(this.boostEvent, e.nativeEvent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const treeNode = this.getTreeNodeFromEvent(e);
|
||||||
|
if (!treeNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { node } = treeNode;
|
||||||
|
const designer = treeNode.designer;
|
||||||
|
const doc = node.document;
|
||||||
|
const selection = doc.selection;
|
||||||
|
const id = node.id;
|
||||||
|
const isMulti = e.metaKey || e.ctrlKey;
|
||||||
|
designer.activeTracker.track(node);
|
||||||
|
if (isMulti && !isRootNode(node) && selection.has(id)) {
|
||||||
|
selection.remove(id);
|
||||||
|
} else {
|
||||||
|
selection.select(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onMouseOver = (e: ReactMouseEvent) => {
|
||||||
|
this.hover(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
private getTreeNodeFromEvent(e: ReactMouseEvent) {
|
||||||
|
if (!this.shell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = getTreeNodeIdByEvent(e, this.shell);
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tree } = this.props;
|
||||||
|
return tree.getTreeNodeById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ignoreUpSelected = false;
|
||||||
|
private boostEvent?: MouseEvent;
|
||||||
|
private onMouseDown = (e: ReactMouseEvent) => {
|
||||||
|
const treeNode = this.getTreeNodeFromEvent(e);
|
||||||
if (!treeNode) {
|
if (!treeNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
edging.watch(treeNode.node);
|
const { node } = treeNode;
|
||||||
}
|
const designer = treeNode.designer;
|
||||||
|
const doc = node.document;
|
||||||
onClick(e: any) {
|
const selection = doc.selection;
|
||||||
if (this.dragEvent && (this.dragEvent as any).shaken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMulti = e.metaKey || e.ctrlKey;
|
const isMulti = e.metaKey || e.ctrlKey;
|
||||||
|
const isLeftButton = e.button === 0;
|
||||||
|
|
||||||
const treeNode = tree.getTreeNodeByEvent(e);
|
if (isLeftButton && !isRootNode(node)) {
|
||||||
|
let nodes: Node[] = [node];
|
||||||
if (!treeNode) {
|
this.ignoreUpSelected = false;
|
||||||
return;
|
if (isMulti) {
|
||||||
|
// multi select mode, directily add
|
||||||
|
if (!selection.has(node.id)) {
|
||||||
|
designer.activeTracker.track(node);
|
||||||
|
selection.add(node.id);
|
||||||
|
this.ignoreUpSelected = true;
|
||||||
}
|
}
|
||||||
|
selection.remove(doc.rootNode.id);
|
||||||
treeNode.select(isMulti);
|
// 获得顶层 nodes
|
||||||
|
nodes = selection.getTopNodes();
|
||||||
// 通知主画板滚动到对应位置
|
|
||||||
activeTracker.track(treeNode.node);
|
|
||||||
}
|
}
|
||||||
|
this.boostEvent = e.nativeEvent;
|
||||||
onMouseOver(e: any) {
|
designer.dragon.boost(
|
||||||
if (dragon.dragging) {
|
{
|
||||||
return;
|
type: DragObjectType.Node,
|
||||||
|
nodes,
|
||||||
|
},
|
||||||
|
this.boostEvent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hover(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseUp(e: any) {
|
|
||||||
if (dragon.dragging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hover(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseLeave() {
|
|
||||||
edging.watch(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
if (this.ref.current) {
|
|
||||||
dragon.from(this.ref.current, (e: MouseEvent) => {
|
|
||||||
this.dragEvent = e;
|
|
||||||
|
|
||||||
const treeNode = tree.getTreeNodeByEvent(e);
|
|
||||||
if (treeNode) {
|
|
||||||
return {
|
|
||||||
type: DragTargetType.Nodes,
|
|
||||||
nodes: [treeNode.node],
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
return null;
|
private onMouseLeave = () => {
|
||||||
});
|
const { tree } = this.props;
|
||||||
}
|
const doc = tree.document;
|
||||||
}
|
doc.designer.hovering.leave(doc);
|
||||||
*/
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { tree } = this.props;
|
const { tree } = this.props;
|
||||||
const root = tree.root;
|
const root = tree.root;
|
||||||
return (
|
return (
|
||||||
<div className="lc-outline-tree">
|
<div
|
||||||
|
className="lc-outline-tree"
|
||||||
|
ref={shell => (this.shell = shell)}
|
||||||
|
onMouseDown={this.onMouseDown}
|
||||||
|
onMouseOver={this.onMouseOver}
|
||||||
|
onClick={this.onClick}
|
||||||
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
>
|
||||||
<TreeNodeView key={root.id} treeNode={root} />
|
<TreeNodeView key={root.id} treeNode={root} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user