From 4edf3fce6bd76612ebc59f5dea4c098361e3f8d8 Mon Sep 17 00:00:00 2001 From: kangwei Date: Thu, 26 Mar 2020 03:42:35 +0800 Subject: [PATCH] outline ok --- .../simulator/host/bem-tools/insertion.less | 3 + .../simulator/host/bem-tools/insertion.tsx | 48 +- .../src/builtins/simulator/host/host.ts | 241 +++++--- packages/designer/src/designer/designer.ts | 12 +- .../src/designer/document/document-model.ts | 61 +- .../designer/document/node/exclusive-group.ts | 4 +- .../designer/document/node/node-children.ts | 2 +- .../src/designer/document/node/node.ts | 22 +- .../designer/src/designer/helper/dragon.ts | 20 +- .../designer/src/designer/helper/location.ts | 29 +- .../designer/src/designer/helper/scroller.ts | 11 +- packages/editor/src/config/assets.js | 2 +- packages/globals/src/utils/index.ts | 1 + packages/globals/src/utils/unique-id.ts | 4 + .../src/helper/dwell-timer.ts | 59 +- .../src/helper/indent-track.ts | 53 ++ .../src/helper/x-axis-tracker.ts | 111 ---- packages/plugin-outline-tree/src/main.ts | 585 +++++++++++++++++- packages/plugin-outline-tree/src/sensor.ts | 220 ------- packages/plugin-outline-tree/src/tree-node.ts | 97 +-- packages/plugin-outline-tree/src/tree.ts | 4 + .../plugin-outline-tree/src/views/style.less | 50 +- .../src/views/tree-branches.tsx | 32 +- .../src/views/tree-node.tsx | 4 +- .../src/views/tree-title.tsx | 15 +- .../plugin-outline-tree/src/views/tree.tsx | 175 ++++-- 26 files changed, 1193 insertions(+), 672 deletions(-) create mode 100644 packages/globals/src/utils/unique-id.ts create mode 100644 packages/plugin-outline-tree/src/helper/indent-track.ts delete mode 100644 packages/plugin-outline-tree/src/helper/x-axis-tracker.ts delete mode 100644 packages/plugin-outline-tree/src/sensor.ts diff --git a/packages/designer/src/builtins/simulator/host/bem-tools/insertion.less b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.less index b5cb19f50..8921e9334 100644 --- a/packages/designer/src/builtins/simulator/host/bem-tools/insertion.less +++ b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.less @@ -20,4 +20,7 @@ width: 3px; height: auto; } + &.invalid { + background-color: red; + } } diff --git a/packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx index f36993253..b3c427a2a 100644 --- a/packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx +++ b/packages/designer/src/builtins/simulator/host/bem-tools/insertion.tsx @@ -2,7 +2,7 @@ import { Component } from 'react'; import { computed, observer } from '../../../../../../globals'; import { SimulatorContext } from '../context'; import { SimulatorHost } from '../host'; -import Location, { +import DropLocation, { Rect, isLocationChildrenDetail, LocationChildrenDetail, @@ -23,15 +23,14 @@ interface InsertionData { /** * 处理拖拽子节点(INode)情况 */ -function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: LocationChildrenDetail): InsertionData { +function processChildrenDetail(sim: ISimulator, container: NodeParent, detail: LocationChildrenDetail): InsertionData { let edge = detail.edge || null; - if (edge) { - edge = sim.computeRect(target); - } - if (!edge) { - return {}; + edge = sim.computeRect(container); + if (!edge) { + return {}; + } } const ret: any = { @@ -42,18 +41,33 @@ function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: Loca if (detail.near) { const { node, pos, rect, align } = detail.near; ret.nearRect = rect || sim.computeRect(node); - ret.vertical = align ? align === 'V' : isVertical(ret.nearRect); - ret.insertType = pos; + 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.vertical = align ? align === 'V' : isVertical(ret.nearRect); + } return ret; } // from outline-tree: has index, but no near // TODO: think of shadowNode & ConditionFlow 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) { // index = 0, eg. nochild, - nearNode = target.children.get(index > 0 ? index - 1 : 0); + nearNode = container.children.get(index > 0 ? index - 1 : 0); if (!nearNode) { ret.insertType = 'cover'; ret.coverRect = edge; @@ -63,7 +77,14 @@ function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: Loca } if (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); + } else { + ret.insertType = 'cover'; + ret.coverRect = edge; } return ret; } @@ -71,7 +92,7 @@ function processChildrenDetail(sim: ISimulator, target: NodeParent, detail: Loca /** * 将 detail 信息转换为页面"坐标"信息 */ -function processDetail({ target, detail, document }: Location): InsertionData { +function processDetail({ target, detail, document }: DropLocation): InsertionData { const sim = document.simulator; if (!sim) { return {}; @@ -115,6 +136,9 @@ export class InsertionView extends Component { } let className = 'lc-insertion'; + if ((loc.detail as any)?.valid === false) { + className += ' invalid'; + } const style: any = {}; let x: number; let y: number; diff --git a/packages/designer/src/builtins/simulator/host/host.ts b/packages/designer/src/builtins/simulator/host/host.ts index d8feb15ea..feb888c79 100644 --- a/packages/designer/src/builtins/simulator/host/host.ts +++ b/packages/designer/src/builtins/simulator/host/host.ts @@ -3,7 +3,7 @@ import { ISimulator, Component, NodeInstance } from '../../../designer/simulator import Viewport from './viewport'; import { createSimulator } from './create-simulator'; 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 ResourceConsumer from './resource-consumer'; import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType } from '../utils/asset'; @@ -536,7 +536,7 @@ export class SimulatorHost implements ISimulator { /** * 通过 DOM 节点获取节点,依赖 simulator 的接口 */ - getNodeInstanceFromElement(target: Element | null): NodeInstance | null { + getNodeInstanceFromElement(target: Element | null): NodeInstance | null { if (!target) { return null; } @@ -701,31 +701,24 @@ export class SimulatorHost implements ISimulator { locate(e: LocateEvent): any { this.sensing = true; this.scroller.scrolling(e); - const dropTarget = this.getDropTarget(e); - if (!dropTarget) { + const dropContainer = this.getDropContainer(e); + if (!dropContainer) { return null; } - if (isLocationData(dropTarget)) { - return this.designer.createLocation(dropTarget); + if (isLocationData(dropContainer)) { + 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 targetInstance = e.targetInstance as ReactInstance; - const parentInstance = this.getClosestNodeInstance(targetInstance, target.id); - const edge = this.computeComponentInstanceRect( - parentInstance?.instance as any, - parentInstance?.node?.componentMeta.rectSelector, - ); + const edge = this.computeComponentInstanceRect(containerInstance, container.componentMeta.rectSelector); if (!edge) { return null; } - const children = target.children; + const children = container.children; const detail: LocationChildrenDetail = { type: LocationDetailType.Children, @@ -734,8 +727,10 @@ export class SimulatorHost implements ISimulator { }; const locationData = { - target, + target: container, detail, + source: 'simulator' + this.document.id, + event: e, }; if (!children || children.size < 1 || !edge) { @@ -755,7 +750,7 @@ export class SimulatorHost implements ISimulator { const instances = this.getComponentInstances(node); const inst = instances ? 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] : null; const rect = inst ? this.computeComponentInstanceRect(inst, node.componentMeta.rectSelector) : null; @@ -830,61 +825,109 @@ export class SimulatorHost implements ISimulator { return this.designer.createLocation(locationData); } - getDropTarget(e: LocateEvent): NodeParent | LocationData | null { + /** + * 查找合适的投放容器 + */ + getDropContainer(e: LocateEvent): DropContainer | LocationData | null { const { target, dragObject } = e; const isAny = isDragAnyObject(dragObject); - let container: any; + const { modalNode, currentRoot } = this.document; + let container: Node; + let nodeInstance: NodeInstance | undefined; if (target) { const ref = this.getNodeInstanceFromElement(target); if (ref?.node) { - e.targetInstance = ref.instance; - e.targetNode = ref.node; + nodeInstance = ref; container = ref.node; } else if (isAny) { return null; } else { - container = this.document.rootNode; + container = currentRoot; } } else if (isAny) { return null; } else { - container = this.document.rootNode; + container = currentRoot; } - if (!isNodeParent(container) && !isRootNode(container)) { - container = container.parent; + if (!isNodeParent(container)) { + container = container.parent || currentRoot; } + // check container if in modalNode layer, if not, use modalNode + if (modalNode && !modalNode.contains(container)) { + container = modalNode; + } + + // TODO: use spec container to accept specialData if (isAny) { - // TODO: use spec container to accept specialData + // will return locationData return null; } + // get common parent, avoid drop container contains by dragObject + // TODO: renderengine support pointerEvents: none for acceleration + const drillDownExcludes = new Set(); + if (isDragNodeObject(dragObject)) { + const nodes = dragObject.nodes; + let i = nodes.length; + let p: any = container; + while (i-- > 0) { + if (contains(nodes[i], p)) { + p = nodes[i].parent; + } + } + if (p !== container) { + container = p || this.document.rootNode; + drillDownExcludes.add(container); + } + } + + const ret: any = { + container, + }; + if (nodeInstance) { + if (nodeInstance.node === container) { + ret.instance = nodeInstance.instance; + } else { + ret.instance = this.getClosestNodeInstance(nodeInstance.instance as any, container.id)?.instance; + } + } else { + ret.instance = this.getComponentInstances(container)?.[0]; + } + let res: any; let upward: any; - // TODO: improve AT_CHILD logic, mark has checked + // TODO: complete drill down logic 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)) { return res; } if (res === true) { - return container; + return ret; } if (!res) { + drillDownExcludes.add(container); if (upward) { container = upward; upward = null; - } else { + } else if (container.parent) { container = container.parent; + } else { + return null; } } else if (isNode(res)) { - /* else if (res === AT_CHILD) { + /* else if (res === DRILL_DOWN) { if (!upward) { upward = container.parent; } - container = this.getNearByContainer(container, e); + container = this.getNearByContainer(container, drillExcludes, e); if (!container) { container = upward; upward = null; @@ -897,38 +940,71 @@ export class SimulatorHost implements ISimulator { return null; } - acceptNodes(container: NodeParent, e: LocateEvent) { - const { dragObject } = e; - if (isRootNode(container)) { - return this.checkDropTarget(container, dragObject as any); + isAcceptable(container: NodeParent): boolean { + return false; + /* + const meta = container.componentMeta; + const instance: any = this.document.getView(container); + if (instance && '$accept' in instance) { + return true; } - - const config = container.componentMeta; - - if (!config.isContainer) { - 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); + return meta.acceptable; + */ } - /* - getNearByContainer(container: NodeParent, e: LocateEvent) { + /** + * 控制接受 + */ + handleAccept({ container, instance }: DropContainer, e: LocateEvent) { + const { dragObject } = e; + if (isRootNode(container)) { + return this.document.checkDropTarget(container, dragObject as any); + } + const meta = container.componentMeta; + + // FIXME: get containerInstance for accept logic use + const acceptable: boolean = this.isAcceptable(container); + if (!meta.isContainer && !acceptable) { + return false; + } + + // first use accept + if (acceptable) { + /* + const view: any = this.document.getView(container); + if (view && '$accept' in view) { + if (view.$accept === false) { + return false; + } + if (view.$accept === AT_CHILD || view.$accept === '@CHILD') { + return AT_CHILD; + } + if (typeof view.$accept === 'function') { + const ret = view.$accept(container, e); + if (ret || ret === false) { + return ret; + } + } + } + if (proto.acceptable) { + const ret = proto.accept(container, e); + if (ret || ret === false) { + return ret; + } + } + */ + } + + // check nesting + return this.document.checkNesting(container, dragObject as any); + } + + /** + * 查找邻近容器 + */ + getNearByContainer(container: NodeParent, e: LocateEvent) { + /* const children = container.children; if (!children || children.length < 1) { return null; @@ -963,43 +1039,7 @@ export class SimulatorHost implements ISimulator { } return nearBy; - } - */ - - checkNesting(dropTarget: NodeParent, dragObject: DragNodeObject | DragNodeDataObject): boolean { - let items: Array; - 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; - 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 } @@ -1065,3 +1105,8 @@ function getMatched(elements: Array, selector: string): Element } return firstQueried; } + +export interface DropContainer { + container: NodeParent; + instance: ReactInstance; +} diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index 34eeeaca7..578d11f81 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -4,7 +4,7 @@ import Project from './project'; import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './helper/dragon'; import ActiveTracker from './helper/active-tracker'; 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 Node, { insertChildren } from './document/node/node'; import { isRootNode } from './document/node/root-node'; @@ -30,7 +30,7 @@ export interface DesignerProps { onMount?: (designer: Designer) => void; onDragstart?: (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; } @@ -158,12 +158,12 @@ export default class Designer { this.props?.eventPipe?.emit(`designer.${event}`, ...args); } - private _dropLocation?: Location; + private _dropLocation?: DropLocation; /** * 创建插入位置,考虑放到 dragon 中 */ - createLocation(locationData: LocationData): Location { - const loc = new Location(locationData); + createLocation(locationData: LocationData): DropLocation { + const loc = new DropLocation(locationData); if (this._dropLocation && this._dropLocation.document !== loc.document) { this._dropLocation.document.internalSetDropLocation(null); } @@ -290,7 +290,7 @@ export default class Designer { return this.project.schema; } - set schema(schema: ProjectSchema) { + setSchema(schema: ProjectSchema) { // todo: } diff --git a/packages/designer/src/designer/document/document-model.ts b/packages/designer/src/designer/document/document-model.ts index 7a244bd54..7d7b31123 100644 --- a/packages/designer/src/designer/document/document-model.ts +++ b/packages/designer/src/designer/document/document-model.ts @@ -1,9 +1,9 @@ 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 RootNode from './node/root-node'; import { ISimulator } from '../simulator'; -import Location from '../helper/location'; +import DropLocation from '../helper/location'; import { ComponentMeta } from '../component-meta'; import History from '../helper/history'; import Prop from './node/props/prop'; @@ -16,7 +16,9 @@ import { computed, obx, autorun, + isNodeSchema, } from '../../../../globals'; +import { isDragNodeDataObject, DragNodeObject, DragNodeDataObject } from '../helper/dragon'; export default class DocumentModel { /** @@ -56,6 +58,15 @@ export default class DocumentModel { 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) { autorun(() => { this.nodes.forEach(item => { @@ -201,11 +212,11 @@ export default class DocumentModel { 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; } @@ -378,6 +389,48 @@ export default class DocumentModel { remove() { // todo: } + + checkNesting(dropTarget: NodeParent, dragObject: DragNodeObject | DragNodeDataObject): boolean { + let items: Array; + 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; + 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 { diff --git a/packages/designer/src/designer/document/node/exclusive-group.ts b/packages/designer/src/designer/document/node/exclusive-group.ts index 92233b6f2..8170b6570 100644 --- a/packages/designer/src/designer/document/node/exclusive-group.ts +++ b/packages/designer/src/designer/document/node/exclusive-group.ts @@ -3,9 +3,11 @@ import { uniqueId } from '../../../../../utils/unique-id'; import Node from './node'; 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 { readonly isExclusiveGroup = true; - readonly id = uniqueId('cond-grp'); + readonly id = uniqueId('exclusive'); @obx.val readonly children: Node[] = []; @obx private visibleIndex = 0; diff --git a/packages/designer/src/designer/document/node/node-children.ts b/packages/designer/src/designer/document/node/node-children.ts index d0eb01792..89257abf1 100644 --- a/packages/designer/src/designer/document/node/node-children.ts +++ b/packages/designer/src/designer/document/node/node-children.ts @@ -39,11 +39,11 @@ export default class NodeChildren { } else { node = this.owner.document.createNode(item); } - node.internalSetParent(this.owner); children[i] = node; } this.children = children; + this.interalInitParent(); } /** diff --git a/packages/designer/src/designer/document/node/node.ts b/packages/designer/src/designer/document/node/node.ts index 0af5522de..2e0738f36 100644 --- a/packages/designer/src/designer/document/node/node.ts +++ b/packages/designer/src/designer/document/node/node.ts @@ -28,12 +28,13 @@ import ExclusiveGroup, { isExclusiveGroup } from './exclusive-group'; * loop * loopArgs * condition - * ------- future support ----- - * conditionGroup - * title - * ignored - * locked - * hidden + * ------- addition support ----- + * conditionGroup use for condition, for exclusive + * title display on outline + * ignored ignore this node will not publish to render, but will store + * locked can not select/hover/ item on canvas but can control on outline + * hidden not visible on canvas + * slotArgs like loopArgs, for slot node */ export default class Node { /** @@ -49,11 +50,12 @@ export default class Node { /** * 节点组件类型 * 特殊节点: - * * #text 文字节点 - * * #expression 表达式节点 * * Page 页面 - * * Block/Fragment 区块 + * * Block 区块 * * Component 组件/元件 + * * Fragment 碎片节点,无 props,有指令 + * * Leaf 文字节点 | 表达式节点,无 props,无指令? + * * Slot 插槽节点,无 props,正常 children,有 slotArgs,有指令 */ readonly componentName: string; /** @@ -240,6 +242,8 @@ export default class Node { if (!isExclusiveGroup(grp)) { if (this.prevSibling?.conditionGroup?.name === grp) { grp = this.prevSibling.conditionGroup; + } else if (this.nextSibling?.conditionGroup?.name === grp) { + grp = this.nextSibling.conditionGroup; } else { grp = new ExclusiveGroup(grp); } diff --git a/packages/designer/src/designer/helper/dragon.ts b/packages/designer/src/designer/helper/dragon.ts index ca748caee..5983b4b0d 100644 --- a/packages/designer/src/designer/helper/dragon.ts +++ b/packages/designer/src/designer/helper/dragon.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import Location from './location'; +import DropLocation from './location'; import DocumentModel from '../document/document-model'; import { ISimulator, isSimulator, ComponentInstance } from '../simulator'; import Node from '../document/node/node'; @@ -47,9 +47,6 @@ export interface LocateEvent { * 事件订正标识,初始构造时,从发起端构造,缺少 canvasX,canvasY, 需要经过订正才有 */ 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 emptyImage: HTMLImageElement = new Image(); + // private emptyImage: HTMLImageElement = new Image(); 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) { @@ -280,6 +277,7 @@ export default class Dragon { let lastArrive: any; const drag = (e: MouseEvent | DragEvent) => { + // FIXME: donot setcopy when: newbie & no location checkcopy(e); if (isInvalidPoint(e, lastArrive)) return; @@ -433,6 +431,7 @@ export default class Dragon { const chooseSensor = (e: LocateEvent) => { let sensor = e.sensor && e.sensor.isEnter(e) ? e.sensor : sensors.find(s => s.sensorAvailable && s.isEnter(e)); if (!sensor) { + // TODO: enter some area like componentspanel cancel if (lastSensor) { sensor = lastSensor; } else if (e.sensor) { @@ -539,13 +538,6 @@ export default class Dragon { }); } - /** - * 是否拷贝态 - */ - private isCopyState(): boolean { - return cursor.isCopy(); - } - /** * 清除所有态:拖拽态、拷贝态 */ diff --git a/packages/designer/src/designer/helper/location.ts b/packages/designer/src/designer/helper/location.ts index c8e06de92..0b527f93c 100644 --- a/packages/designer/src/designer/helper/location.ts +++ b/packages/designer/src/designer/helper/location.ts @@ -1,9 +1,12 @@ import ComponentNode, { NodeParent } from '../document/node/node'; import DocumentModel from '../document/document-model'; +import { LocateEvent } from './dragon'; export interface LocationData { target: NodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode detail: LocationDetail; + source: string; + event: LocateEvent; } export enum LocationDetailType { @@ -13,14 +16,19 @@ export enum LocationDetailType { export interface LocationChildrenDetail { type: LocationDetailType.Children; - index: number; + index?: number | null; + /** + * 是否有效位置 + */ + valid?: boolean; edge?: DOMRect; near?: { node: ComponentNode; - pos: 'before' | 'after'; + pos: 'before' | 'after' | 'replace'; rect?: Rect; align?: 'V' | 'H'; }; + focus?: { type: 'slots' } | { type: 'node'; node: NodeParent }; } export interface LocationPropDetail { @@ -118,15 +126,28 @@ export function getWindow(elem: Element | Document): Window { return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!; } -export default class Location { +export default class DropLocation { readonly target: NodeParent; readonly detail: LocationDetail; + readonly event: LocateEvent; + readonly source: string; get document(): DocumentModel { return this.target.document; } - constructor({ target, detail }: LocationData) { + constructor({ target, detail, source, event }: LocationData) { this.target = target; 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, + }); } } diff --git a/packages/designer/src/designer/helper/scroller.ts b/packages/designer/src/designer/helper/scroller.ts index a901ab10b..e55f91e88 100644 --- a/packages/designer/src/designer/helper/scroller.ts +++ b/packages/designer/src/designer/helper/scroller.ts @@ -43,8 +43,8 @@ const SCROLL_ACCURCY = 30; export interface IScrollable { scrollTarget?: ScrollTarget | Element; - bounds: DOMRect; - scale: number; + bounds?: DOMRect | null; + scale?: number; } export default class Scroller { @@ -62,8 +62,7 @@ export default class Scroller { return target; } - constructor(private scrollable: IScrollable) { - } + constructor(private scrollable: IScrollable) {} scrollTo(options: { left?: number; top?: number }) { this.cancel(); @@ -119,9 +118,9 @@ export default class Scroller { scrolling(point: { globalX: number; globalY: number }) { this.cancel(); - const { bounds, scale } = this.scrollable; + const { bounds, scale = 1 } = this.scrollable; const scrollTarget = this.scrollTarget; - if (!scrollTarget) { + if (!scrollTarget || !bounds) { return; } diff --git a/packages/editor/src/config/assets.js b/packages/editor/src/config/assets.js index d8506368f..3249541db 100644 --- a/packages/editor/src/config/assets.js +++ b/packages/editor/src/config/assets.js @@ -165,7 +165,7 @@ export default { name: 'children', propType: 'node' } - ] + ], }, 'Button.Group': { componentName: 'Button.Group', diff --git a/packages/globals/src/utils/index.ts b/packages/globals/src/utils/index.ts index 365722a71..cc0173e34 100644 --- a/packages/globals/src/utils/index.ts +++ b/packages/globals/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './create-icon'; export * from './is-react'; +export * from './unique-id'; diff --git a/packages/globals/src/utils/unique-id.ts b/packages/globals/src/utils/unique-id.ts new file mode 100644 index 000000000..3713cbd06 --- /dev/null +++ b/packages/globals/src/utils/unique-id.ts @@ -0,0 +1,4 @@ +let guid = Date.now(); +export function uniqueId(prefix = '') { + return `${prefix}${(guid++).toString(36).toLowerCase()}`; +} diff --git a/packages/plugin-outline-tree/src/helper/dwell-timer.ts b/packages/plugin-outline-tree/src/helper/dwell-timer.ts index e629018ef..77b9622a8 100644 --- a/packages/plugin-outline-tree/src/helper/dwell-timer.ts +++ b/packages/plugin-outline-tree/src/helper/dwell-timer.ts @@ -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 { 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) {} - /** - * 根据传入的 ID 判断,停留事件是否触发 - * 如果上一次的标示(包括不存在)和这次不相同,则设置停留计时器 - * 反之什么也不用做 - */ - start(id: any, fn: () => void) { - if (this.previous !== id) { - this.end(); - this.previous = id; - this.timer = setTimeout(() => { - fn(); - this.end(); - }, this.timeout) as number; + focus(node: NodeParent, event: LocateEvent) { + this.event = event; + if (this.previous === node) { + return; + } + this.reset(); + this.previous = node; + const x = Date.now(); + console.info('set', x); + this.timer = setTimeout(() => { + console.info('done', x, Date.now() - x); + this.previous && this.decide(this.previous, this.event!); + 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() { - const timer = this.timer; - if (timer) { - clearTimeout(timer); + reset() { + console.info('reset'); + if (this.timer) { + clearTimeout(this.timer); this.timer = undefined; } - if (this.previous) { - this.previous = undefined; - } + this.previous = undefined; } } diff --git a/packages/plugin-outline-tree/src/helper/indent-track.ts b/packages/plugin-outline-tree/src/helper/indent-track.ts new file mode 100644 index 000000000..2e97005bb --- /dev/null +++ b/packages/plugin-outline-tree/src/helper/indent-track.ts @@ -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; + } +} diff --git a/packages/plugin-outline-tree/src/helper/x-axis-tracker.ts b/packages/plugin-outline-tree/src/helper/x-axis-tracker.ts deleted file mode 100644 index 27dc58d90..000000000 --- a/packages/plugin-outline-tree/src/helper/x-axis-tracker.ts +++ /dev/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; - } -} diff --git a/packages/plugin-outline-tree/src/main.ts b/packages/plugin-outline-tree/src/main.ts index 91cdb5620..3f95eae45 100644 --- a/packages/plugin-outline-tree/src/main.ts +++ b/packages/plugin-outline-tree/src/main.ts @@ -1,25 +1,63 @@ -import { computed, obx } from '../../globals'; +import { computed, obx, uniqueId } from '../../globals'; 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 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 { constructor(readonly designer: Designer) { - designer.dragon.onDragstart((e) => { + designer.dragon.onDragstart(e => { const tree = this.currentTree; if (tree) { tree.document.selection.getTopNodes().forEach(node => { 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(); + addBoard(board: IScrollBoard) { + this.boards.add(board); + } + removeBoard(board: IScrollBoard) { + this.boards.delete(board); } private treeMap = new Map(); @@ -49,12 +87,13 @@ function getTreeMaster(designer: Designer): TreeMaster { return master; } -export class OutlineMain implements ISensor { +export class OutlineMain implements ISensor, IScrollBoard, IScrollable { private _designer?: Designer; @obx.ref private _master?: TreeMaster; get master() { return this._master; } + readonly id = uniqueId('tree'); constructor(readonly editor: any) { if (editor.designer) { @@ -66,30 +105,467 @@ export class OutlineMain implements ISensor { } } + /** + * @see ISensor + */ fixEvent(e: LocateEvent): LocateEvent { - throw new Error("Method not implemented."); + if (e.fixed) { + return e; + } + + const notMyEvent = e.originalEvent.view?.document !== document; + + if (!e.target || notMyEvent) { + e.target = document.elementFromPoint(e.canvasX!, e.canvasY!); + } + + // documentModel : 目标文档 + e.documentModel = this._designer?.currentDocument; + + // 事件已订正 + e.fixed = true; + return e; } - locate(e: LocateEvent): Location | undefined { - throw new Error("Method not implemented."); + 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 { - 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 { - throw new Error("Method not implemented."); + private tryScrollAgain: number | null = null; + /** + * @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) { this._designer = designer; this._master = getTreeMaster(designer); - // designer.dragon.addSensor(this); + this._master.addBoard(this); + designer.dragon.addSensor(this); + this.scroller = designer.createScroller(this); } purge() { this._designer?.dragon.removeSensor(this); + this._master?.removeBoard(this); // todo purge treeMaster if needed } @@ -108,9 +584,80 @@ export class OutlineMain implements ISensor { } this._shell = 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, + }; +} diff --git a/packages/plugin-outline-tree/src/sensor.ts b/packages/plugin-outline-tree/src/sensor.ts deleted file mode 100644 index a0b92615d..000000000 --- a/packages/plugin-outline-tree/src/sensor.ts +++ /dev/null @@ -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, - }, - }); - } -} diff --git a/packages/plugin-outline-tree/src/tree-node.ts b/packages/plugin-outline-tree/src/tree-node.ts index 300ffebf3..ff095eeb9 100644 --- a/packages/plugin-outline-tree/src/tree-node.ts +++ b/packages/plugin-outline-tree/src/tree-node.ts @@ -1,7 +1,7 @@ import { computed, obx, TitleContent, isI18nData, localeFormat } from '../../globals'; import Node from '../../designer/src/designer/document/node/node'; 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 { Tree } from './tree'; @@ -14,15 +14,15 @@ export default class TreeNode { * 是否可以展开 */ @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; - 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 { @@ -44,6 +44,14 @@ export default class TreeNode { 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) { 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 document: DocumentModel; @obx.ref private _node: Node; diff --git a/packages/plugin-outline-tree/src/tree.ts b/packages/plugin-outline-tree/src/tree.ts index e445ea2fc..689ae2524 100644 --- a/packages/plugin-outline-tree/src/tree.ts +++ b/packages/plugin-outline-tree/src/tree.ts @@ -25,4 +25,8 @@ export class Tree { this.treeNodesMap.set(node.id, treeNode); return treeNode; } + + getTreeNodeById(id: string) { + return this.treeNodesMap.get(id); + } } diff --git a/packages/plugin-outline-tree/src/views/style.less b/packages/plugin-outline-tree/src/views/style.less index a13fa5167..0d615878d 100644 --- a/packages/plugin-outline-tree/src/views/style.less +++ b/packages/plugin-outline-tree/src/views/style.less @@ -14,8 +14,9 @@ } .lc-outline-tree { + @treeNodeHeight: 30px; overflow: hidden; - margin-bottom: 20px; + margin-bottom: @treeNodeHeight; user-select: none; .tree-node-branches::before { @@ -28,6 +29,7 @@ left: 6px; content: ' '; z-index: 2; + pointer-events: none; } &:hover { @@ -37,10 +39,15 @@ } .insertion { - pointer-events: none !important; + pointer-events: all !important; border: 1px dashed var(--color-brand-light); - height: 18px; + height: @treeNodeHeight; + box-sizing: border-box; transform: translateZ(0); + &.invalid { + border-color: red; + background-color: rgba(240, 154, 154, 0.719); + } } .condition-group-container { @@ -75,7 +82,7 @@ .tree-node-slots { border-bottom: 1px solid rgb(144, 94, 190); position: relative; - &:before { + &::before { position: absolute; display: block; width: 0; @@ -99,6 +106,16 @@ 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 { @@ -146,7 +163,8 @@ border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1)); display: flex; align-items: center; - height: 30px; + height: @treeNodeHeight; + box-sizing: border-box; position: relative; transform: translateZ(0); padding-right: 5px; @@ -196,6 +214,12 @@ opacity: 0.5; } } + html.lc-cursor-dragging & { + // FIXME: only hide hover shows + .tree-node-hide-btn, .tree-node-lock-btn { + display: none; + } + } &.editing { & > .tree-node-hide-btn, & >.tree-node-lock-btn { display: none; @@ -285,6 +309,22 @@ & > .tree-node-branches::before { 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 { diff --git a/packages/plugin-outline-tree/src/views/tree-branches.tsx b/packages/plugin-outline-tree/src/views/tree-branches.tsx index 25d904367..41cee4e76 100644 --- a/packages/plugin-outline-tree/src/views/tree-branches.tsx +++ b/packages/plugin-outline-tree/src/views/tree-branches.tsx @@ -1,5 +1,6 @@ import { observer, Title } from '../../../globals'; import { Component } from 'react'; +import classNames from 'classnames'; import TreeNode from '../tree-node'; import TreeNodeView from './tree-node'; import ExclusiveGroup from '../../../designer/src/designer/document/node/exclusive-group'; @@ -55,7 +56,16 @@ class TreeNodeChildren extends Component<{ groupContents = []; } }; - const { dropIndex } = treeNode; + const dropDetail = treeNode.dropDetail; + const dropIndex = dropDetail?.index; + const insertion = ( +
+ ); treeNode.children?.forEach((child, index) => { const { conditionGroup } = child.node; if (conditionGroup !== currentGrp) { @@ -66,22 +76,23 @@ class TreeNodeChildren extends Component<{ currentGrp = conditionGroup; if (index === dropIndex) { if (groupContents.length > 0) { - groupContents.push(
); + groupContents.push(insertion); } else { - children.push(
); + children.push(insertion); } } groupContents.push(); } else { if (index === dropIndex) { - children.push(
); + children.push(insertion); } children.push(); } }); endGroup(); - if (dropIndex != null && dropIndex === treeNode.children?.length) { - children.push(
); + const length = treeNode.children?.length || 0; + if (dropIndex != null && dropIndex >= length) { + children.push(insertion); } return
{children}
; @@ -101,9 +112,14 @@ class TreeNodeSlots extends Component<{ return null; } return ( -
+
- + <Title title={{ type: 'i18n', intl: intl('Slots') }} /> </div> {treeNode.slots.map(tnode => ( <TreeNodeView key={tnode.id} treeNode={tnode} /> diff --git a/packages/plugin-outline-tree/src/views/tree-node.tsx b/packages/plugin-outline-tree/src/views/tree-node.tsx index b18a9688a..66dc2a078 100644 --- a/packages/plugin-outline-tree/src/views/tree-node.tsx +++ b/packages/plugin-outline-tree/src/views/tree-node.tsx @@ -27,10 +27,10 @@ export default class TreeNodeView extends Component<{ treeNode: TreeNode }> { // 是否锁定的 locked: treeNode.locked, // 是否投放响应 - dropping: treeNode.dropIndex != null, + dropping: treeNode.dropDetail?.index != null, 'is-root': treeNode.isRoot(), 'condition-flow': treeNode.node.conditionGroup != null, - // highlight: treeNode.isResponseDropping() && treeNode.dropIndex == null, + highlight: treeNode.isFocusingNode(), }); return ( diff --git a/packages/plugin-outline-tree/src/views/tree-title.tsx b/packages/plugin-outline-tree/src/views/tree-title.tsx index 2e89551fc..a3bad394e 100644 --- a/packages/plugin-outline-tree/src/views/tree-title.tsx +++ b/packages/plugin-outline-tree/src/views/tree-title.tsx @@ -61,7 +61,8 @@ export default class TreeTitle extends Component<{ const { treeNode } = this.props; const { editing } = this.state; const isCNode = !treeNode.isRoot(); - const isNodeParent = treeNode.node.isNodeParent; + const { node } = treeNode; + const isNodeParent = node.isNodeParent; let style: any; if (isCNode) { const depth = treeNode.depth; @@ -77,9 +78,9 @@ export default class TreeTitle extends Component<{ className={classNames('tree-node-title', { editing, })} - ref={ref => treeNode.mount(ref)} style={style} - onClick={treeNode.node.conditionGroup ? () => treeNode.node.setConditionalVisible() : undefined} + data-id={treeNode.id} + onClick={node.conditionGroup ? () => node.setConditionalVisible() : undefined} > {isCNode && <ExpandBtn treeNode={treeNode} />} <div className="tree-node-icon">{createIcon(treeNode.icon)}</div> @@ -94,19 +95,19 @@ export default class TreeTitle extends Component<{ ) : ( <Fragment> <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 */} <IconSlot /> - <EmbedTip>{intl('Slot for {prop}', { prop: treeNode.node.slotFor.key })}</EmbedTip> + <EmbedTip>{intl('Slot for {prop}', { prop: node.slotFor.key })}</EmbedTip> </a>)} - {treeNode.node.hasLoop() && ( + {node.hasLoop() && ( <a className="tree-node-tag loop"> {/* todo: click todo something */} <IconLoop /> <EmbedTip>{intl('Loop')}</EmbedTip> </a> )} - {treeNode.node.hasCondition() && !treeNode.node.conditionGroup && ( + {node.hasCondition() && !node.conditionGroup && ( <a className="tree-node-tag cond"> {/* todo: click todo something */} <IconCond /> diff --git a/packages/plugin-outline-tree/src/views/tree.tsx b/packages/plugin-outline-tree/src/views/tree.tsx index bc219cf41..3349f8761 100644 --- a/packages/plugin-outline-tree/src/views/tree.tsx +++ b/packages/plugin-outline-tree/src/views/tree.tsx @@ -1,83 +1,140 @@ -import { Component } from 'react'; +import { Component, MouseEvent as ReactMouseEvent } from 'react'; import { observer } from '../../../globals'; import { Tree } from '../tree'; 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 export default class TreeView extends Component<{ tree: Tree }> { - /* - hover(e: any) { - const treeNode = tree.getTreeNodeByEvent(e); + private shell: HTMLDivElement | null = null; + private hover(e: ReactMouseEvent) { + 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) { return; } - edging.watch(treeNode.node); - } - - onClick(e: any) { - if (this.dragEvent && (this.dragEvent as any).shaken) { - return; - } + const { node } = treeNode; + const designer = treeNode.designer; + const doc = node.document; + const selection = doc.selection; const isMulti = e.metaKey || e.ctrlKey; + const isLeftButton = e.button === 0; - const treeNode = tree.getTreeNodeByEvent(e); - - if (!treeNode) { - return; - } - - treeNode.select(isMulti); - - // 通知主画板滚动到对应位置 - activeTracker.track(treeNode.node); - } - - onMouseOver(e: any) { - if (dragon.dragging) { - return; - } - - 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], - }; + if (isLeftButton && !isRootNode(node)) { + let nodes: Node[] = [node]; + this.ignoreUpSelected = false; + if (isMulti) { + // multi select mode, directily add + if (!selection.has(node.id)) { + designer.activeTracker.track(node); + selection.add(node.id); + this.ignoreUpSelected = true; } - return null; - }); + selection.remove(doc.rootNode.id); + // 获得顶层 nodes + nodes = selection.getTopNodes(); + } + this.boostEvent = e.nativeEvent; + designer.dragon.boost( + { + type: DragObjectType.Node, + nodes, + }, + this.boostEvent, + ); } - } - */ + }; + + private onMouseLeave = () => { + const { tree } = this.props; + const doc = tree.document; + doc.designer.hovering.leave(doc); + }; render() { const { tree } = this.props; const root = tree.root; 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} /> </div> );