outline ok

This commit is contained in:
kangwei 2020-03-26 03:42:35 +08:00
parent 9a2e5e97de
commit 4edf3fce6b
26 changed files with 1193 additions and 672 deletions

View File

@ -20,4 +20,7 @@
width: 3px; width: 3px;
height: auto; height: auto;
} }
&.invalid {
background-color: red;
}
} }

View File

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

View File

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

View File

@ -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:
} }

View File

@ -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 {

View File

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

View File

@ -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();
} }
/** /**

View File

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

View File

@ -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();
}
/** /**
* *
*/ */

View File

@ -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,
});
} }
} }

View File

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

View File

@ -165,7 +165,7 @@ export default {
name: 'children', name: 'children',
propType: 'node' propType: 'node'
} }
] ],
}, },
'Button.Group': { 'Button.Group': {
componentName: 'Button.Group', componentName: 'Button.Group',

View File

@ -1,2 +1,3 @@
export * from './create-icon'; export * from './create-icon';
export * from './is-react'; export * from './is-react';
export * from './unique-id';

View File

@ -0,0 +1,4 @@
let guid = Date.now();
export function uniqueId(prefix = '') {
return `${prefix}${(guid++).toString(36).toLowerCase()}`;
}

View File

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

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

View File

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

View File

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

View File

@ -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,
},
});
}
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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,7 +112,12 @@ 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>

View File

@ -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 (

View File

@ -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 />

View File

@ -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>
); );