This commit is contained in:
kangwei 2020-02-15 01:26:27 +08:00
parent b566524466
commit a49ec4a171
8 changed files with 255 additions and 973 deletions

View File

@ -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<string, INode>();
private nodes = new Set<INode>();
private nodesMap = new Map<string, Node>();
private nodes = new Set<Node>();
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 {}
}

View File

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

View File

@ -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<any, any>;
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<INode | NodeData> = 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<INode | NodeData> = 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);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,74 @@
export interface SimulatorInterface<P = object> {
/**
*
*/
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;
}