From a49ec4a17133cfaa2c948402f8aad8bd8324e0c8 Mon Sep 17 00:00:00 2001 From: kangwei Date: Sat, 15 Feb 2020 01:26:27 +0800 Subject: [PATCH] sim 20% --- .../src/designer/document/document-context.ts | 180 +++-- .../src/designer/document/location.ts | 6 +- .../src/designer/document/master-board.ts | 733 ------------------ .../src/designer/document/scroller.ts | 134 ---- .../src/designer/document/selection.ts | 94 +-- .../src/designer/document/viewport.ts | 2 +- packages/designer/src/designer/project.ts | 5 +- .../src/designer/simulator-interface.ts | 74 ++ 8 files changed, 255 insertions(+), 973 deletions(-) delete mode 100644 packages/designer/src/designer/document/master-board.ts delete mode 100644 packages/designer/src/designer/document/scroller.ts diff --git a/packages/designer/src/designer/document/document-context.ts b/packages/designer/src/designer/document/document-context.ts index f6c2ffc7f..b284cb851 100644 --- a/packages/designer/src/designer/document/document-context.ts +++ b/packages/designer/src/designer/document/document-context.ts @@ -1,8 +1,16 @@ import Project from '../project'; -import { RootSchema, NodeData, isDOMText, isJSExpression } from '../schema'; -import Node from './node/node'; +import { RootSchema, NodeData, isDOMText, isJSExpression, NodeSchema } from '../schema'; +import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './node/node'; +import { Selection } from './selection'; +import RootNode from './node/root-node'; +import { SimulatorInterface } from '../simulator-interface'; +import { computed } from '@recore/obx'; export default class DocumentContext { + /** + * 根节点 类型有:Page/Component/Block + */ + readonly rootNode: RootNode; /** * 文档编号 */ @@ -14,23 +22,35 @@ export default class DocumentContext { /** * 操作记录控制 */ - readonly history: History = new History(this); - /** - * 根节点 类型有:Page/Component/Block - */ - readonly root: Root; + // TODO + // readonly history: History = new History(this); /** * 模拟器 */ simulator?: SimulatorInterface; - private nodesMap = new Map(); - private nodes = new Set(); + private nodesMap = new Map(); + private nodes = new Set(); private seqId = 0; + get fileName() { + return this.rootNode.extras.get('fileName')?.value as string; + } + + set fileName(fileName: string) { + this.rootNode.extras.get('fileName', true).value = fileName; + } + constructor(readonly project: Project, schema: RootSchema) { - this.id = uniqueId('doc'); - this.root = new Root(this, viewData); + this.rootNode = new RootNode(this, schema); + this.id = this.rootNode.id; + } + + /** + * 生成唯一id + */ + nextId() { + return (++this.seqId).toString(36).toLocaleLowerCase(); } /** @@ -40,19 +60,22 @@ export default class DocumentContext { return this.nodesMap.get(id) || null; } + /** + * 是否存在节点 + */ + hasNode(id: string): boolean { + const node = this.getNode(id); + return node ? !node.isPurged : false; + } + /** * 根据 schema 创建一个节点 */ createNode(data: NodeData): Node { let schema: any; - if (isDOMText(data)) { + if (isDOMText(data) || isJSExpression(data)) { schema = { - componentName: '#text', - children: data, - }; - } else if (isJSExpression(data)) { - schema = { - componentName: '#expression', + componentName: '#frag', children: data, }; } else { @@ -63,12 +86,21 @@ export default class DocumentContext { this.nodes.add(node); return node; } + /** * 插入一个节点 */ - insertNode(parent: Node, thing: Node | Schema, at?: number | null, copy?: boolean): Node { - + insertNode(parent: NodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node { + return insertChild(parent, thing, at, copy); } + + /** + * 插入多个节点 + */ + insertNodes(parent: NodeParent, thing: Node[] | NodeData[], at?: number | null, copy?: boolean) { + return insertChildren(parent, thing, at, copy); + } + /** * 移除一个节点 */ @@ -82,30 +114,88 @@ export default class DocumentContext { node = idOrNode; id = node.id; } - if (!node || !node.parent) { + if (!node) { return; } - // TODO: 考虑留着缓存 - this.nodesMap.delete(id); - this.nodes.delete(node); - node.parent.removeChild(node); + this.internalRemoveAndPurgeNode(node); } + + /** + * 内部方法,请勿调用 + */ + internalRemoveAndPurgeNode(node: Node) { + if (!this.nodes.has(node)) { + return; + } + this.nodesMap.delete(node.id); + this.nodes.delete(node); + node.remove(); + } + + /** + * 包裹当前选区中的节点 + */ + wrapWith(schema: NodeSchema): Node | null { + const nodes = this.selection.getTopNodes(); + if (nodes.length < 1) { + return null; + } + const wrapper = this.createNode(schema); + if (isNodeParent(wrapper)) { + const first = nodes[0]; + // TODO: check nesting rules x 2 + insertChild(first.parent!, wrapper, first.index); + insertChildren(wrapper, nodes); + this.selection.select(wrapper.id); + return wrapper; + } + + this.removeNode(wrapper); + return null; + } + /** * 导出 schema 数据 */ - getSchema(): Schema { - return this.root.getSchema(); + get schema(): NodeSchema { + return this.rootNode.schema; } + /** - * 导出节点 Schema + * 导出节点数据 */ - getNodeSchema(id: string): Schema | null { + getNodeSchema(id: string): NodeData | null { const node = this.getNode(id); if (node) { - return node.getSchema(); + return node.schema; } return null; } + + /** + * 是否已修改 + */ + isModified() { + // return !this.history.isSavePoint(); + } + + @computed get simulatorProps(): object { + let simulatorProps = this.project.simulatorProps; + if (typeof simulatorProps === 'function') { + simulatorProps = simulatorProps(this); + } + return { + ...simulatorProps, + documentContext: this, + onMount: this.mountSimulator.bind(this), + }; + } + + private mountSimulator(simulator: SimulatorInterface) { + this.simulator = simulator; + // TODO: emit simulator mounted + } + /** * 根据节点取得视图实例,在循环等场景会有多个,依赖 simulator 的接口 */ @@ -115,6 +205,7 @@ export default class DocumentContext { } return null; } + /** * 通过 DOM 节点获取节点,依赖 simulator 的接口 */ @@ -129,6 +220,7 @@ export default class DocumentContext { } return this.getNode(id) as Node; } + /** * 获得到的结果是一个数组 * 表示一个实例对应多个外层 DOM 节点,依赖 simulator 的接口 @@ -144,34 +236,28 @@ export default class DocumentContext { return this.simulator.findDOMNodes(viewInstance); } + + getComponent(componentName: string): any { + return this.simulator!.getCurrentComponent(componentName); + } + /** - * 激活当前文档 + * 激活 */ active(): void {} + /** * 不激活 */ suspense(): void {} - /** - * 销毁 - */ - destroy(): void {} /** - * 是否已修改 + * 开启 */ - isModified() { - return !this.history.isSavePoint(); - } + open(): void {} /** - * 生成唯一id + * 关闭 */ - nextId() { - return (++this.seqId).toString(36).toLocaleLowerCase(); - } - - getComponent(tagName: string): any { - return this.simulator!.getCurrentComponent(tagName); - } + close(): void {} } diff --git a/packages/designer/src/designer/document/location.ts b/packages/designer/src/designer/document/location.ts index 030565920..aedab4b27 100644 --- a/packages/designer/src/designer/document/location.ts +++ b/packages/designer/src/designer/document/location.ts @@ -1,8 +1,8 @@ -import { INode, INodeParent } from './node/node'; +import { INode, NodeParent } from './node/node'; import DocumentContext from './document-context'; export interface LocationData { - target: INodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode + target: NodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode detail: LocationDetail; } @@ -113,7 +113,7 @@ export function getWindow(elem: Element | Document): Window { } export default class Location { - readonly target: INodeParent; + readonly target: NodeParent; readonly detail: LocationDetail; constructor(readonly document: DocumentContext, { target, detail }: LocationData) { diff --git a/packages/designer/src/designer/document/master-board.ts b/packages/designer/src/designer/document/master-board.ts deleted file mode 100644 index b9c53879b..000000000 --- a/packages/designer/src/designer/document/master-board.ts +++ /dev/null @@ -1,733 +0,0 @@ -import RootNode from './root-node'; -import { flags, panes } from '../globals'; -import { - dragon, - ISenseAble, - isShaken, - LocateEvent, - isNodesDragTarget, - NodesDragTarget, - NodeDatasDragTarget, - isNodeDatasDragTarget, - DragTargetType, - isAnyDragTarget, -} from '../globals/dragon'; -import cursor from '../utils/cursor'; -import { - INode, - isElementNode, - isNode, - INodeParent, - insertChildren, - hasConditionFlow, - contains, - isRootNode, - isConfettiNode, -} from './node/node'; -import { - Point, - Rect, - getRectTarget, - isChildInline, - isRowContainer, - LocationDetailType, - LocationChildrenDetail, - isLocationChildrenDetail, - LocationData, - isLocationData, -} from './location'; -import { isConditionFlow } from './node/condition-flow'; -import { isElementData, NodeData } from './document-data'; -import ElementNode from './node/element-node'; -import { AT_CHILD } from '../prototype/prototype'; -import Scroller from './scroller'; -import { isShadowNode } from './node/shadow-node'; -import { activeTracker } from '../globals/active-tracker'; -import { edging } from '../globals/edging'; -import { setNativeSelection } from '../utils/navtive-selection'; -import DocumentContext from './document-context'; -import Simulator from '../adaptors/simulator'; -import { focusing } from '../globals/focusing'; -import embedEditor from '../globals/embed-editor'; - -export const MasterBoardID = 'master-board'; -export default class MasterBoard implements ISenseAble { - id = MasterBoardID; - sensitive = true; - readonly contentDocument: Document; - - private simulator: Simulator; - private sensing = false; - private scroller: Scroller; - - get bounds() { - const vw = this.document.viewport; - const bounds = vw.bounds; - const innerBounds = vw.innerBounds; - const doe = this.contentDocument.documentElement; - return { - top: bounds.top, - left: bounds.left, - right: bounds.right, - bottom: bounds.bottom, - width: bounds.width, - height: bounds.height, - innerBounds, - scale: vw.scale, - scrollHeight: doe.scrollHeight, - scrollWidth: doe.scrollWidth, - }; - } - - constructor(readonly document: DocumentContext, frame: HTMLIFrameElement) { - this.simulator = document.simulator!; - this.contentDocument = frame.contentDocument!; - this.scroller = new Scroller(this, document.viewport.scrollTarget!); - const doc = this.contentDocument; - const selection = document.selection; - - // TODO: think of lock when edit a node - // 事件路由 - doc.addEventListener('mousedown', (downEvent: MouseEvent) => { - /* - if (embedEditor.editing) { - return; - } - */ - const target = document.getNodeFromElement(downEvent.target as Element); - panes.dockingStation.visible = false; - focusing.focus('canvas'); - if (!target) { - selection.clear(); - return; - } - - const isMulti = downEvent.metaKey || downEvent.ctrlKey; - const isLeftButton = downEvent.which === 1 || downEvent.button === 0; - - if (isLeftButton) { - let node: INode = target; - if (hasConditionFlow(node)) { - node = node.conditionFlow; - } - let nodes: INode[] = [node]; - let ignoreUpSelected = false; - if (isMulti) { - // multi select mode, directily add - if (!selection.has(node.id)) { - activeTracker.track(node); - selection.add(node.id); - ignoreUpSelected = true; - } - // 获得顶层 nodes - nodes = selection.getTopNodes(); - } else if (selection.containsNode(target)) { - nodes = selection.getTopNodes(); - } else { - // will clear current selection & select dragment in dragstart - } - dragon.boost( - { - type: DragTargetType.Nodes, - nodes, - }, - downEvent, - ); - if (ignoreUpSelected) { - // multi select mode has add selected, should return - return; - } - } - - const checkSelect = (e: MouseEvent) => { - doc.removeEventListener('mouseup', checkSelect, true); - if (!isShaken(downEvent, e)) { - // const node = hasConditionFlow(target) ? target.conditionFlow : target; - const node = target; - const id = node.id; - activeTracker.track(node); - if (isMulti && selection.has(id)) { - selection.del(id); - } else { - selection.select(id); - } - } - }; - doc.addEventListener('mouseup', checkSelect, true); - }); - - dragon.onDragstart(({ dragTarget }) => { - if (this.disableEdging) { - this.disableEdging(); - } - if (isNodesDragTarget(dragTarget) && dragTarget.nodes.length === 1) { - // ensure current selecting - selection.select(dragTarget.nodes[0].id); - } - flags.setDragComponentsMode(true); - }); - - dragon.onDragend(({ dragTarget, copy }) => { - const loc = this.document.dropLocation; - flags.setDragComponentsMode(false); - if (loc) { - if (!isConditionFlow(loc.target)) { - if (isLocationChildrenDetail(loc.detail)) { - let nodes: INode[] | undefined; - if (isNodesDragTarget(dragTarget)) { - nodes = insertChildren(loc.target, dragTarget.nodes, loc.detail.index, copy); - } else if (isNodeDatasDragTarget(dragTarget)) { - // process nodeData - const nodesData = this.document.processDocumentData(dragTarget.data, dragTarget.maps); - nodes = insertChildren(loc.target, nodesData, loc.detail.index); - } - if (nodes) { - this.document.selection.selectAll(nodes.map(o => o.id)); - setTimeout(() => activeTracker.track(nodes![0]), 10); - } - } - // TODO: others... - } - } - this.document.clearLocation(); - this.enableEdging(); - }); - - // cause edit - doc.addEventListener('dblclick', (e: MouseEvent) => { - // TODO: refactor - let target = document.getNodeFromElement(e.target as Element)!; - if (target && isElementNode(target)) { - if (isShadowNode(target)) { - target = target.origin; - } - if (target.children.length === 1 && isConfettiNode(target.children[0])) { - // test - // embedEditor.edit(target as any, 'children', document.getDOMNodes(target) as any); - - activeTracker.track(target.children[0]); - selection.select(target.children[0].id); - } - } - }); - - activeTracker.onChange(({ node, detail }) => { - this.scrollToNode(node, detail); - }); - - this.enableEdging(); - } - - private disableEdging: (() => void) | undefined; - - enableEdging() { - const edgingWatch = (e: Event) => { - const node = this.document.getNodeFromElement(e.target as Element); - edging.watch(node); - e.stopPropagation(); - }; - const leave = () => edging.watch(null); - - this.contentDocument.addEventListener('mouseover', edgingWatch, true); - this.contentDocument.addEventListener('mouseleave', leave, false); - - // TODO: refactor this line, contains click, mousedown, mousemove - this.contentDocument.addEventListener( - 'mousemove', - (e: Event) => { - e.stopPropagation(); - }, - true, - ); - - this.disableEdging = () => { - edging.watch(null); - this.contentDocument.removeEventListener('mouseover', edgingWatch, true); - this.contentDocument.removeEventListener('mouseleave', leave, false); - }; - } - - setNativeSelection(enableFlag: boolean) { - setNativeSelection(enableFlag); - this.simulator.utils.setNativeSelection(enableFlag); - } - - setDragging(flag: boolean) { - cursor.setDragging(flag); - this.simulator.utils.cursor.setDragging(flag); - } - - setCopy(flag: boolean) { - cursor.setCopy(flag); - this.simulator.utils.cursor.setCopy(flag); - } - - isCopy(): boolean { - return this.simulator.utils.cursor.isCopy(); - } - - releaseCursor() { - cursor.release(); - this.simulator.utils.cursor.release(); - } - - getDropTarget(e: LocateEvent): INodeParent | LocationData | null { - const { target, dragTarget } = e; - const isAny = isAnyDragTarget(dragTarget); - let container: any; - if (target) { - const ref = this.document.getNodeFromElement(target as Element); - if (ref) { - container = ref; - } else if (isAny) { - return null; - } else { - container = this.document.view; - } - } else if (isAny) { - return null; - } else { - container = this.document.view; - } - - if (!isElementNode(container) && !isRootNode(container)) { - container = container.parent; - } - - // use spec container to accept specialData - if (isAny) { - while (container) { - if (isRootNode(container)) { - return null; - } - const locationData = this.acceptAnyData(container, e); - if (locationData) { - return locationData; - } - container = container.parent; - } - return null; - } - - let res: any; - let upward: any; - // TODO: improve AT_CHILD logic, mark has checked - while (container) { - res = this.acceptNodes(container, e); - if (isLocationData(res)) { - return res; - } - if (res === true) { - return container; - } - if (!res) { - if (upward) { - container = upward; - upward = null; - } else { - container = container.parent; - } - } else if (res === AT_CHILD) { - if (!upward) { - upward = container.parent; - } - container = this.getNearByContainer(container, e); - if (!container) { - container = upward; - upward = null; - } - } else if (isNode(res)) { - container = res; - upward = null; - } - } - return null; - } - - acceptNodes(container: RootNode | ElementNode, e: LocateEvent) { - const { dragTarget } = e; - if (isRootNode(container)) { - return this.checkDropTarget(container, dragTarget as any); - } - - const proto = container.prototype; - - const acceptable: boolean = this.isAcceptable(container); - if (!proto.isContainer && !acceptable) { - return false; - } - - // check is contains, get common parent - if (isNodesDragTarget(dragTarget)) { - const nodes = dragTarget.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.view; - } - } - - // 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; - } - } - } - - return this.checkNesting(container, dragTarget as any); - } - - getNearByContainer(container: INodeParent, e: LocateEvent): INodeParent | null { - const children = container.children; - if (!children || children.length < 1) { - return null; - } - - let nearDistance: any = null; - let nearBy: any = null; - for (let i = 0, l = children.length; i < l; i++) { - let child: any = children[i]; - if (!isElementNode(child)) { - continue; - } - if (hasConditionFlow(child)) { - const bn = child.conditionFlow; - i = bn.index + bn.length - 1; - child = bn.visibleNode; - } - const rect = this.document.computeRect(child); - if (!rect) { - continue; - } - - if (isPointInRect(e, rect)) { - return child; - } - - const distance = distanceToRect(e, rect); - if (nearDistance === null || distance < nearDistance) { - nearDistance = distance; - nearBy = child; - } - } - - return nearBy; - } - - locate(e: LocateEvent): any { - this.sensing = true; - this.scroller.scrolling(e); - const dropTarget = this.getDropTarget(e); - if (!dropTarget) { - return null; - } - - if (isLocationData(dropTarget)) { - return this.document.createLocation(dropTarget); - } - - const target = dropTarget; - - const edge = this.document.computeRect(target); - - const children = target.children; - - const detail: LocationChildrenDetail = { - type: LocationDetailType.Children, - index: 0, - }; - - const locationData = { - target, - detail, - }; - - if (!children || children.length < 1 || !edge) { - return this.document.createLocation(locationData); - } - - let nearRect = null; - let nearIndex = 0; - let nearNode = null; - let nearDistance = null; - let top = null; - let bottom = null; - - for (let i = 0, l = children.length; i < l; i++) { - let node = children[i]; - let index = i; - if (hasConditionFlow(node)) { - node = node.conditionFlow; - index = node.index; - // skip flow items - i = index + (node as any).length - 1; - } - const rect = this.document.computeRect(node); - - if (!rect) { - continue; - } - - const distance = isPointInRect(e, rect) ? 0 : distanceToRect(e, rect); - - if (distance === 0) { - nearDistance = distance; - nearNode = node; - nearIndex = index; - nearRect = rect; - break; - } - - // TODO: 忘记为什么这么处理了,记得添加注释 - if (top === null || rect.top < top) { - top = rect.top; - } - if (bottom === null || rect.bottom > bottom) { - bottom = rect.bottom; - } - - if (nearDistance === null || distance < nearDistance) { - nearDistance = distance; - nearNode = node; - nearIndex = index; - nearRect = rect; - } - } - - detail.index = nearIndex; - - if (nearNode && nearRect) { - const el = getRectTarget(nearRect); - const inline = el ? isChildInline(el) : false; - const row = el ? isRowContainer(el.parentElement!) : false; - const vertical = inline || row; - // TODO: fix type - const near: any = { - node: nearNode, - pos: 'before', - align: vertical ? 'V' : 'H', - }; - detail.near = near; - if (isNearAfter(e, nearRect, vertical)) { - near.pos = 'after'; - detail.index = nearIndex + (isConditionFlow(nearNode) ? nearNode.length : 1); - } - if (!row && nearDistance !== 0) { - const edgeDistance = distanceToEdge(e, edge); - if (edgeDistance.distance < nearDistance!) { - const nearAfter = edgeDistance.nearAfter; - if (top == null) { - top = edge.top; - } - if (bottom == null) { - bottom = edge.bottom; - } - near.rect = new DOMRect(edge.left, top, edge.width, bottom - top); - near.align = 'H'; - near.pos = nearAfter ? 'after' : 'before'; - detail.index = nearAfter ? children.length : 0; - } - } - } - - return this.document.createLocation(locationData); - } - - private tryScrollAgain: number | null = null; - scrollToNode(node: INode, detail?: any, tryTimes = 0) { - this.tryScrollAgain = null; - if (this.sensing) { - // actived sensor - return; - } - - const opt: any = {}; - let scroll = false; - - if (detail) { - // TODO: - /* - const rect = insertion ? insertion.getNearRect() : node.getRect(); - let y; - let scroll = false; - if (insertion && rect) { - y = insertion.isNearAfter() ? rect.bottom : rect.top; - - if (y < bounds.top || y > bounds.bottom) { - scroll = true; - } - }*/ - } else { - const rect = this.document.computeRect(node); - if (!rect || rect.width === 0 || rect.height === 0) { - if (!this.tryScrollAgain && tryTimes < 3) { - this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(node, null, tryTimes + 1)); - } - return; - } - const scrollTarget = this.document.viewport.scrollTarget!; - const st = scrollTarget.top; - const sl = scrollTarget.left; - const { innerBounds, scrollHeight, scrollWidth } = this.bounds; - const { height, width, top, bottom, left, right } = innerBounds; - - if (rect.height > height ? rect.top > bottom || rect.bottom < top : rect.top < top || rect.bottom > bottom) { - opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height); - scroll = true; - } - - if (rect.width > width ? rect.left > right || rect.right < left : rect.left < left || rect.right > right) { - opt.left = Math.min(rect.left + rect.width / 2 + sl - left - width / 2, scrollWidth - width); - scroll = true; - } - } - - if (scroll && this.scroller) { - this.scroller.scrollTo(opt); - } - } - - fixEvent(e: LocateEvent): LocateEvent { - if (e.fixed) { - return e; - } - if (!e.target || e.originalEvent.view!.document !== this.contentDocument) { - e.target = this.contentDocument.elementFromPoint(e.clientX, e.clientY); - } - return e; - } - - isEnter(e: LocateEvent): boolean { - const rect = this.bounds; - return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right; - } - - inRange(e: LocateEvent): boolean { - return e.globalX <= this.bounds.right; - } - - deactive() { - this.sensing = false; - this.scroller.cancel(); - } - - isAcceptable(container: ElementNode): boolean { - const proto = container.prototype; - const view: any = this.document.getView(container); - if (view && '$accept' in view) { - return true; - } - return proto.acceptable; - } - - acceptAnyData(container: ElementNode, e: LocateEvent | MouseEvent | KeyboardEvent) { - const proto = container.prototype; - const view: any = this.document.getView(container); - // use view instance method: $accept - if (view && typeof view.$accept === 'function') { - // should return LocationData - return view.$accept(container, e); - } - // use prototype method: accept - return proto.accept(container, e); - } - - checkNesting(dropTarget: ElementNode, dragTarget: NodesDragTarget | NodeDatasDragTarget): boolean { - const items: Array = dragTarget.nodes || (dragTarget as NodeDatasDragTarget).data; - return items.every(item => this.checkNestingDown(dropTarget, item)); - } - - checkDropTarget(dropTarget: RootNode | ElementNode, dragTarget: NodesDragTarget | NodeDatasDragTarget): boolean { - const items: Array = dragTarget.nodes || (dragTarget as NodeDatasDragTarget).data; - return items.every(item => this.checkNestingUp(dropTarget, item)); - } - - checkNestingUp(parent: RootNode | ElementNode, target: NodeData | INode): boolean { - if (isElementNode(target) || isElementData(target)) { - const proto = isElementNode(target) - ? target.prototype - : this.document.getPrototypeByTagNameOrURI(target.tagName, target.uri); - if (proto) { - return proto.checkNestingUp(target, parent); - } - } - - return true; - } - - checkNestingDown(parent: ElementNode, target: NodeData | INode): boolean { - const proto = parent.prototype; - if (isConditionFlow(parent)) { - return parent.children.every( - child => proto.checkNestingDown(parent, child) && this.checkNestingUp(parent, child), - ); - } else { - return proto.checkNestingDown(parent, target) && this.checkNestingUp(parent, target); - } - } -} - -function isPointInRect(point: Point, rect: Rect) { - return ( - point.clientY >= rect.top && - point.clientY <= rect.bottom && - point.clientX >= rect.left && - point.clientX <= rect.right - ); -} - -function distanceToRect(point: Point, rect: Rect) { - let minX = Math.min(Math.abs(point.clientX - rect.left), Math.abs(point.clientX - rect.right)); - let minY = Math.min(Math.abs(point.clientY - rect.top), Math.abs(point.clientY - rect.bottom)); - if (point.clientX >= rect.left && point.clientX <= rect.right) { - minX = 0; - } - if (point.clientY >= rect.top && point.clientY <= rect.bottom) { - minY = 0; - } - - return Math.sqrt(minX ** 2 + minY ** 2); -} - -function distanceToEdge(point: Point, rect: Rect) { - const distanceTop = Math.abs(point.clientY - rect.top); - const distanceBottom = Math.abs(point.clientY - rect.bottom); - - return { - distance: Math.min(distanceTop, distanceBottom), - nearAfter: distanceBottom < distanceTop, - }; -} - -function isNearAfter(point: Point, rect: Rect, inline: boolean) { - if (inline) { - return ( - Math.abs(point.clientX - rect.left) + Math.abs(point.clientY - rect.top) > - Math.abs(point.clientX - rect.right) + Math.abs(point.clientY - rect.bottom) - ); - } - return Math.abs(point.clientY - rect.top) > Math.abs(point.clientY - rect.bottom); -} diff --git a/packages/designer/src/designer/document/scroller.ts b/packages/designer/src/designer/document/scroller.ts deleted file mode 100644 index f7409511c..000000000 --- a/packages/designer/src/designer/document/scroller.ts +++ /dev/null @@ -1,134 +0,0 @@ -export class ScrollTarget { - get left() { - return 'scrollX' in this.target ? this.target.scrollX : this.target.scrollLeft; - } - get top() { - return 'scrollY' in this.target ? this.target.scrollY : this.target.scrollTop; - } - scrollTo(options: { left?: number; top?: number }) { - this.target.scrollTo(options); - } - - scrollToXY(x: number, y: number) { - this.target.scrollTo(x, y); - } - - constructor(private target: Window | Element) {} -} - -function easing(n: number) { - return Math.sin((n * Math.PI) / 2); -} - -const SCROLL_ACCURCY = 30; - -export default class Scroller { - private pid: number | undefined; - - constructor(private board: { bounds: any }, private scrollTarget: ScrollTarget) {} - - scrollTo(options: { left?: number; top?: number }) { - this.cancel(); - - let pid: number; - const left = this.scrollTarget.left; - const top = this.scrollTarget.top; - const end = () => { - this.cancel(); - }; - - if ((left === options.left || options.left == null) && top === options.top) { - end(); - return; - } - - const duration = 200; - const start = +new Date(); - - const animate = () => { - if (pid !== this.pid) { - return; - } - - const now = +new Date(); - const time = Math.min(1, (now - start) / duration); - const eased = easing(time); - const opt: any = {}; - if (options.left != null) { - opt.left = eased * (options.left - left) + left; - } - if (options.top != null) { - opt.top = eased * (options.top - top) + top; - } - - this.scrollTarget.scrollTo(opt); - - if (time < 1) { - this.pid = pid = requestAnimationFrame(animate); - } else { - end(); - } - }; - - this.pid = pid = requestAnimationFrame(animate); - } - - scrolling(point: { globalX: number; globalY: number }) { - this.cancel(); - - const x = point.globalX; - const y = point.globalY; - const bounds = this.board.bounds; - const scale = bounds.scale; - - const maxScrollHeight = bounds.scrollHeight - bounds.height / scale; - const maxScrollWidth = bounds.scrollWidth - bounds.width / scale; - let sx = this.scrollTarget.left; - let sy = this.scrollTarget.top; - let ax = 0; - let ay = 0; - if (y < bounds.top + SCROLL_ACCURCY) { - ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale; - } else if (y > bounds.bottom - SCROLL_ACCURCY) { - ay = Math.min(Math.max(y + SCROLL_ACCURCY - bounds.bottom, 10), 50) / scale; - } - if (x < bounds.left + SCROLL_ACCURCY) { - ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale; - } else if (x > bounds.right - SCROLL_ACCURCY) { - ax = Math.min(Math.max(x + SCROLL_ACCURCY - bounds.right, 10), 50) / scale; - } - - if (!ax && !ay) { - return; - } - - const animate = () => { - let scroll = false; - if ((ay > 0 && sy < maxScrollHeight) || (ay < 0 && sy > 0)) { - sy += ay; - sy = Math.min(Math.max(sy, 0), maxScrollHeight); - scroll = true; - } - if ((ax > 0 && sx < maxScrollWidth) || (ax < 0 && sx > 0)) { - sx += ax; - sx = Math.min(Math.max(sx, 0), maxScrollWidth); - scroll = true; - } - if (!scroll) { - return; - } - - this.scrollTarget.scrollTo({ left: sx, top: sy }); - this.pid = requestAnimationFrame(animate); - }; - - animate(); - } - - cancel() { - if (this.pid) { - cancelAnimationFrame(this.pid); - } - this.pid = undefined; - } -} diff --git a/packages/designer/src/designer/document/selection.ts b/packages/designer/src/designer/document/selection.ts index 3d64cb356..fba7959ed 100644 --- a/packages/designer/src/designer/document/selection.ts +++ b/packages/designer/src/designer/document/selection.ts @@ -1,5 +1,5 @@ -import { INode, contains, isNode, comparePosition } from './node/node'; -import { obx } from '@ali/recore'; +import Node, { comparePosition } from './node/node'; +import { obx } from '@recore/obx'; import DocumentContext from './document-context'; export class Selection { @@ -7,6 +7,9 @@ export class Selection { constructor(private doc: DocumentContext) {} + /** + * 选中 + */ select(id: string) { if (this.selected.length === 1 && this.selected.indexOf(id) > -1) { // avoid cause reaction @@ -16,76 +19,83 @@ export class Selection { this.selected = [id]; } + /** + * 批量选中 + */ selectAll(ids: string[]) { this.selected = ids; } + /** + * 清除选中 + */ clear() { this.selected = []; } + /** + * 整理选中 + */ dispose() { let i = this.selected.length; while (i-- > 0) { const id = this.selected[i]; - const node = this.doc.getNode(id, true); - if (!node) { + if (!this.doc.hasNode(id)) { this.selected.splice(i, 1); - } else if (node.id !== id) { + } else { this.selected[i] = id; } } } + /** + * 添加选中 + */ add(id: string) { if (this.selected.indexOf(id) > -1) { return; } - const i = this.findIndex(id); - if (i > -1) { - this.selected.splice(i, 1); - } - this.selected.push(id); } - private findIndex(id: string): number { - const ns = getNamespace(id); - const nsx = `${ns}:`; - return this.selected.findIndex(idx => { - return idx === ns || idx.startsWith(nsx); - }); + /** + * 是否选中 + */ + has(id: string) { + return this.selected.indexOf(id) > -1; } - has(id: string, variant = false) { - return this.selected.indexOf(id) > -1 || (variant && this.findIndex(id) > -1); - } - - del(id: string, variant = false) { + /** + * 移除选中 + */ + remove(id: string) { let i = this.selected.indexOf(id); if (i > -1) { this.selected.splice(i, 1); - } else if (variant) { - i = this.findIndex(id); - this.selected.splice(i, 1); } } - containsNode(node: INode) { + /** + * 选区是否包含节点 + */ + containsNode(node: Node) { for (const id of this.selected) { const parent = this.doc.getNode(id); - if (parent && contains(parent, node)) { + if (parent?.contains(node)) { return true; } } return false; } + /** + * 获取选中的节点 + */ getNodes() { const nodes = []; for (const id of this.selected) { - const node = this.doc.getNode(id, true); + const node = this.doc.getNode(id); if (node) { nodes.push(node); } @@ -93,24 +103,13 @@ export class Selection { return nodes; } - getOriginNodes(): INode[] { - const nodes: any[] = []; - for (const id of this.selected) { - const node = this.doc.getOriginNode(id); - if (node && !nodes.includes(node)) { - nodes.push(node); - } - } - return nodes; - } - /** - * get union items that at top level + * 获取顶层选区节点, 场景:拖拽时,建立蒙层,只蒙在最上层 */ - getTopNodes(origin?: boolean) { + getTopNodes() { const nodes = []; for (const id of this.selected) { - const node = origin ? this.doc.getOriginNode(id) : this.doc.getNode(id); + const node = this.doc.getNode(id); if (!node) { continue; } @@ -136,16 +135,3 @@ export class Selection { return nodes; } } - -function getNamespace(id: string) { - const i = id.indexOf(':'); - if (i < 0) { - return id; - } - - return id.substring(0, i); -} - -export function isSelectable(obj: any): obj is INode { - return isNode(obj); -} diff --git a/packages/designer/src/designer/document/viewport.ts b/packages/designer/src/designer/document/viewport.ts index f3c510ad2..5692296d3 100644 --- a/packages/designer/src/designer/document/viewport.ts +++ b/packages/designer/src/designer/document/viewport.ts @@ -1,7 +1,7 @@ import { obx } from '@ali/recore'; import { screen } from '../globals/screen'; import { Point } from './location'; -import { ScrollTarget } from './scroller'; +import { ScrollTarget } from '../../builtins/simulator/scroller'; export type AutoFit = '100%'; export const AutoFit = '100%'; diff --git a/packages/designer/src/designer/project.ts b/packages/designer/src/designer/project.ts index a761f438f..5b99d9160 100644 --- a/packages/designer/src/designer/project.ts +++ b/packages/designer/src/designer/project.ts @@ -4,9 +4,12 @@ import { EventEmitter } from 'events'; export default class Project { @obx documents: DocumentContext[]; - displayMode: 'exclusive' | 'split'; // P2 + displayMode: 'exclusive' | 'tabbed' | 'split'; // P2 private emitter = new EventEmitter(); private data: ProjectSchema = {}; + + // 考虑项目级别 History + constructor(schema: ProjectSchema) { this.data = { ...schema }; } diff --git a/packages/designer/src/designer/simulator-interface.ts b/packages/designer/src/designer/simulator-interface.ts index e69de29bb..167df15fd 100644 --- a/packages/designer/src/designer/simulator-interface.ts +++ b/packages/designer/src/designer/simulator-interface.ts @@ -0,0 +1,74 @@ +export interface SimulatorInterface

{ + /** + * 获得边界维度等信息 + */ + readonly bounds: object; + + // containerAssets // like react jQuery lodash + // vendorsAssets // antd/fusion + // themeAssets + // componentAssets + // simulatorUrls // + // layout: ComponentName + // utils, dataSource, constants 模拟 + // 获取区块代码, 通过 components 传递,可异步获取 + setProps(props: P): void; + + /** + * 设置编辑模式 + */ + setDesignMode(mode: "live" | "design" | string): void; + + /** + * 设置拖拽态 + */ + setDraggingState(state: boolean): void; + + /** + * 是否拖拽态 + */ + isDraggingState(): boolean; + + /** + * 设置拷贝态 + */ + setCopyState(state: boolean): void; + + /** + * 是否拷贝态 + */ + isCopyState(): boolean; + + /** + * 清除所有态:拖拽态、拷贝态 + */ + clearState(): void; + + /** + * 在模拟器拖拽定位 + */ + locate(e: LocateEvent): any; + + /** + * 滚动视口到节点 + */ + scrollToNode(node: INode, detail?: any): void; + + /** + * 给 event 打补丁,添加 canvasX, globalX 等信息,用于拖拽 + */ + fixEvent(e: LocateEvent): LocateEvent; + + getComponent(npmInfo: object): ReactComponent | any; + getViewInstance(node: Node): ViewInstance[] | null; + + /** + * 设置挂起 + */ + setSuspense(suspended: boolean): void; + + /** + * 销毁 + */ + destroy(): void; +}