This commit is contained in:
kangwei 2020-02-16 04:27:43 +08:00
parent a49ec4a171
commit eb7a7f519f
14 changed files with 223 additions and 384 deletions

View File

@ -9,27 +9,30 @@
"author": "",
"license": "MIT",
"dependencies": {
"@recore/core-obx": "^1.0.4",
"@recore/obx": "^1.0.5",
"@types/medium-editor": "^5.0.3",
"@ali/lowcode-renderer": "0.9.0",
"classnames": "^2.2.6",
"react": "^16",
"react-dom": "^16.7.0"
},
"devDependencies": {
"@recore/config": "^2.0.0",
"@types/classnames": "^2.2.7",
"@types/jest": "^24.0.16",
"@types/node": "^13.7.1",
"@types/react": "^16",
"@types/react-dom": "^16",
"@recore/config": "^2.0.0",
"ts-jest": "^24.0.2",
"ts-node": "^8.0.1",
"eslint": "^6.5.1",
"husky": "^1.1.2",
"jest": "^23.4.1",
"lint-staged": "^7.1.2",
"prettier": "^1.18.2",
"ts-jest": "^24.0.2",
"ts-node": "^8.0.1",
"tslib": "^1.9.3",
"typescript": "^3.1.3",
"prettier": "^1.18.2"
"typescript": "^3.1.3"
},
"lint-staged": {
"./src/**/*.{ts,tsx}": [

View File

@ -1,13 +0,0 @@
// outline
// insertion
/*
// 插入指示 insertion 竖线 横线 插入块 禁止插入块
线绿
线绿
绿
线
cover
*/

View File

@ -1,80 +0,0 @@
import { getCurrentAdaptor } from '../globals';
import Simulator from '../adaptors/simulator';
import { isCSSUrl } from '../utils/is-css-url';
export interface AssetMap {
jsUrl?: string;
cssUrl?: string;
jsText?: string;
cssText?: string;
}
export type AssetList = string[];
export type Assets = AssetMap[] | AssetList;
function isAssetMap(obj: any): obj is AssetMap {
return obj && typeof obj === 'object';
}
export function createSimulator<T, I>(iframe: HTMLIFrameElement, vendors: Assets = []): Promise<Simulator<T, I>> {
const currentAdaptor = getCurrentAdaptor();
const win: any = iframe.contentWindow;
const doc = iframe.contentDocument!;
const styles: string[] = [];
let scripts: string[] = [];
const afterScripts: string[] = [];
vendors.forEach((asset: any) => {
if (!isAssetMap(asset)) {
if (isCSSUrl(asset)) {
asset = { cssUrl: asset };
} else {
if (asset.startsWith('!')) {
afterScripts.push(`<script src="${asset.slice(1)}"></script>`);
return;
}
asset = { jsUrl: asset };
}
}
if (asset.jsText) {
scripts.push(`<script>${asset.jsText}</script>`);
}
if (asset.jsUrl) {
scripts.push(`<script src="${asset.jsUrl}"></script>`);
}
if (asset.cssUrl) {
styles.push(`<link rel="stylesheet" href="${asset.cssUrl}" />`);
}
if (asset.cssText) {
styles.push(`<style type="text/css">${asset.cssText}</style>`);
}
});
currentAdaptor.simulatorUrls.forEach(url => {
if (isCSSUrl(url)) {
styles.push(`<link rel="stylesheet" href="${url}" />`);
} else {
scripts.push(`<script src="${url}"></script>`);
}
});
scripts = scripts.concat(afterScripts);
doc.open();
doc.write(`<!doctype html><html><head><meta charset="utf-8"/>
${styles.join('\n')}
<style base-point></style>
${scripts.join('\n')}
</head></html>`);
doc.close();
return new Promise(resolve => {
if (win.VisionSimulator) {
return resolve(win.VisionSimulator);
}
const loaded = () => {
resolve(win.VisionSimulator);
win.removeEventListener('load', loaded);
};
win.addEventListener('load', loaded);
});
}

View File

@ -10,6 +10,21 @@ class Designer {
dragboost(locateEvent: LocateEvent): void;
addDropSensor(dropSensor: DropSensor): void;
private _suspensed: boolean = false;
get suspensed(): boolean {
return this._suspensed;
}
set suspensed(flag: boolean) {
this._suspensed = flag;
// Todo afterwards...
if (flag) {
// this.project.suspensed = true?
}
}
// 事件 & 消息
onActiveChange(): () => void;
onDragstart(): void;

View File

@ -24,14 +24,18 @@ export default class DocumentContext {
*/
// TODO
// readonly history: History = new History(this);
/**
*
*/
simulator?: SimulatorInterface;
private nodesMap = new Map<string, Node>();
private nodes = new Set<Node>();
private seqId = 0;
private _simulator?: SimulatorInterface;
/**
*
*/
get simulator(): SimulatorInterface | null {
return this._simulator || null;
}
get fileName() {
return this.rootNode.extras.get('fileName')?.value as string;
@ -179,6 +183,9 @@ export default class DocumentContext {
// return !this.history.isSavePoint();
}
/**
*
*/
@computed get simulatorProps(): object {
let simulatorProps = this.project.simulatorProps;
if (typeof simulatorProps === 'function') {
@ -192,7 +199,7 @@ export default class DocumentContext {
}
private mountSimulator(simulator: SimulatorInterface) {
this.simulator = simulator;
this._simulator = simulator;
// TODO: emit simulator mounted
}
@ -241,23 +248,53 @@ export default class DocumentContext {
return this.simulator!.getCurrentComponent(componentName);
}
/**
*
*/
active(): void {}
private _opened: boolean = true;
private _suspensed: boolean = false;
/**
*
* 
*/
suspense(): void {}
get suspensed(): boolean {
return this._suspensed;
}
/**
*
* suspensed 
*/
open(): void {}
get actived(): boolean {
return !this._suspensed;
}
/**
*
*
* tab
*/
close(): void {}
set suspensed(flag: boolean) {
if (!this._opened && !flag) {
return;
}
this._suspensed = flag;
}
/**
*
*/
open(): void {
this._opened = true;
}
/**
* sleep
*/
close(): void {
this.suspensed = true;
this._opened = false;
}
/**
*
*/
remove() {
}
}

View File

@ -1,4 +1,4 @@
import { INode, NodeParent } from './node/node';
import ComponentNode, { NodeParent } from './node/node';
import DocumentContext from './document-context';
export interface LocationData {
@ -15,7 +15,7 @@ export interface LocationChildrenDetail {
type: LocationDetailType.Children;
index: number;
near?: {
node: INode;
node: ComponentNode;
pos: 'before' | 'after';
rect?: Rect;
align?: 'V' | 'H';

View File

@ -1,96 +0,0 @@
import { obx } from '@ali/recore';
import { screen } from '../globals/screen';
import { Point } from './location';
import { ScrollTarget } from '../../builtins/simulator/scroller';
export type AutoFit = '100%';
export const AutoFit = '100%';
export default class Viewport {
private shell: HTMLDivElement | undefined;
scrollTarget: ScrollTarget | undefined;
get scale(): number {
if (this.width === AutoFit) {
return 1;
}
return screen.width / this.width;
}
get height(): number | AutoFit {
if (this.scale === 1) {
return AutoFit;
}
return screen.height / this.scale;
}
private _bounds: ClientRect | DOMRect | null = null;
get bounds(): ClientRect | DOMRect {
if (this._bounds) {
return this._bounds;
}
this._bounds = this.shell!.getBoundingClientRect();
requestAnimationFrame(() => {
this._bounds = null;
});
return this._bounds;
}
get innerBounds(): ClientRect | DOMRect {
const bounds = this.bounds;
const scale = this.scale;
const ret: any = {
top: 0,
left: 0,
x: 0,
y: 0,
width: bounds.width / scale,
height: bounds.height / scale,
};
ret.right = ret.width;
ret.bottom = ret.height;
return ret;
}
@obx.ref width: number | AutoFit = AutoFit;
@obx.ref scrollX = 0;
@obx.ref scrollY = 0;
setShell(shell: HTMLDivElement) {
this.shell = shell;
}
setScrollTarget(target: Window) {
this.scrollTarget = new ScrollTarget(target);
this.scrollX = this.scrollTarget.left;
this.scrollY = this.scrollTarget.top;
target.onscroll = () => {
this.scrollX = this.scrollTarget!.left;
this.scrollY = this.scrollTarget!.top;
};
}
toGlobalPoint(point: Point): Point {
if (!this.shell) {
return point;
}
const rect = this.shell.getBoundingClientRect();
return {
clientX: point.clientX * this.scale + rect.left,
clientY: point.clientY * this.scale + rect.top,
};
}
toLocalPoint(point: Point): Point {
if (!this.shell) {
return point;
}
const rect = this.shell.getBoundingClientRect();
return {
clientX: (point.clientX - rect.left) / this.scale,
clientY: (point.clientY - rect.top) / this.scale,
};
}
}

View File

@ -1,69 +1,107 @@
import { EventEmitter } from 'events';
import MasterBoard from '../document/master-board';
import Location from '../document/location';
import { INode } from '../document/node';
import { NodeData } from '../document/document-data';
import { getCurrentDocument } from './current';
import { obx } from '@ali/recore';
import { obx } from '@recore/obx';
import Location from './document/location';
import Project from './project';
import DocumentContext from './document/document-context';
import { NodeData } from './schema';
import { SimulatorInterface } from './simulator-interface';
import Node from './document/node/node';
export interface LocateEvent {
readonly type: 'LocateEvent';
readonly clientX: number;
readonly clientY: number;
/**
*
*/
readonly globalX: number;
readonly globalY: number;
/**
*
*/
readonly originalEvent: MouseEvent;
readonly dragTarget: DragTarget;
/**
*
*/
readonly dragObject: DragObject;
/**
*
*/
target: Element | null;
/**
*
*/
canvasX?: number;
canvasY?: number;
/**
*
*/
document?: DocumentContext;
/**
* canvasX,canvasY,
*/
fixed?: true;
}
export type DragTarget = NodesDragTarget | NodeDatasDragTarget | AnyDragTarget;
export enum DragTargetType {
Nodes = 'nodes',
NodeDatas = 'nodedatas',
/**
*
*/
export interface SensorInterface {
/**
* false
*/
readonly sensorAvailable: boolean;
/**
*
*/
fixEvent(e: LocateEvent): LocateEvent;
/**
*
*/
locate(e: LocateEvent): Location | undefined;
/**
*
*/
isEnter(e: LocateEvent): boolean;
/**
*
*/
deactiveSensor(): void;
}
export interface NodesDragTarget {
type: DragTargetType.Nodes;
nodes: INode[];
export type DragObject = DragNodeObject | DragNodeDataObject | DragAnyObject;
export enum DragObjectType {
Node = 'node',
NodeData = 'nodedata',
}
export function isNodesDragTarget(obj: any): obj is NodesDragTarget {
return obj && obj.type === DragTargetType.Nodes;
export interface DragNodeObject {
type: DragObjectType.Node;
node: Node | Node[];
}
export interface NodeDatasDragTarget {
type: DragTargetType.NodeDatas;
data: NodeData[];
export interface DragNodeDataObject {
type: DragObjectType.NodeData;
data: NodeData | NodeData[];
maps?: { [tagName: string]: string };
thumbnail?: string;
description?: string;
[extra: string]: any;
}
export function isNodeDatasDragTarget(obj: any): obj is NodeDatasDragTarget {
return obj && obj.type === DragTargetType.NodeDatas;
}
export interface AnyDragTarget {
export interface DragAnyObject {
type: string;
[key: string]: any;
}
export function isAnyDragTarget(obj: any): obj is AnyDragTarget {
return obj && obj.type !== DragTargetType.NodeDatas && obj.type !== DragTargetType.Nodes;
export function isDragNodeObject(obj: any): obj is DragNodeObject {
return obj && obj.type === DragObjectType.Node;
}
export interface ISenseAble {
id: string;
sensitive: boolean;
fixEvent(e: LocateEvent): LocateEvent;
locate(e: LocateEvent): Location | undefined;
isEnter(e: LocateEvent): boolean;
inRange(e: LocateEvent): boolean;
deactive(): void;
export function isDragNodeDataObject(obj: any): obj is DragNodeDataObject {
return obj && obj.type === DragObjectType.NodeData;
}
export function isDragAnyObject(obj: any): obj is DragAnyObject {
return obj && obj.type !== DragObjectType.NodeData && obj.type !== DragObjectType.Node;
}
export function isLocateEvent(e: any): e is LocateEvent {
@ -92,7 +130,7 @@ function getTopDocument(e: MouseEvent, local: Document) {
return e.view!.document === local ? null : document;
}
class Dragon {
export default class Dragon {
private sensors: ISenseAble[] = [];
/**
@ -113,7 +151,9 @@ class Dragon {
return doc.masterBoard;
}
from(shell: Element, boost: (e: MouseEvent) => DragTarget | null) {
constructor(readonly project: Project) {}
from(shell: Element, boost: (e: MouseEvent) => DragObject | null) {
const mousedown = (e: MouseEvent) => {
// ESC or RightClick
if (e.which === 3 || e.button === 2) {
@ -137,7 +177,7 @@ class Dragon {
/**
* dragTarget should be a INode | INode[] | NodeData | NodeData[]
*/
boost(dragTarget: DragTarget, boostEvent: MouseEvent) {
boost(dragObject: DragObject, boostEvent: MouseEvent) {
if (!this.master) {
return;
}
@ -145,7 +185,7 @@ class Dragon {
const doc = master.contentDocument;
const viewport = master.document.viewport;
const topDoc = getTopDocument(boostEvent, doc);
const newBie = dragTarget.type !== DragTargetType.Nodes;
const newBie = dragObject.type !== DragTargetType.Nodes;
let lastLocation: any = null;
let lastSensor: ISenseAble | undefined;
this.dragging = false;
@ -221,7 +261,7 @@ class Dragon {
if (this.dragging) {
this.dragging = false;
try {
this.emitter.emit('dragend', { dragTarget, copy: master.isCopy() }, lastLocation);
this.emitter.emit('dragend', { dragTarget: dragObject, copy: master.isCopy() }, lastLocation);
} catch (ex) {
exception = ex;
}
@ -255,7 +295,7 @@ class Dragon {
const evt: any = {
type: 'LocateEvent',
target: e.target,
dragTarget,
dragTarget: dragObject,
originalEvent: e,
};
if (e.view!.document === document) {
@ -274,17 +314,29 @@ class Dragon {
return evt;
};
const sensors: ISenseAble[] = ([master] as any).concat(this.sensors);
function getSourceSensor(dragObject: DragObject): SimulatorInterface | null {
if (!isDragNodeObject(dragObject)) {
return null;
}
return (Array.isArray(dragObject.node) ? dragObject.node[0] : dragObject.node)?.document.simulator || null;
}
const simSensors = this.project.documents.map(doc => (doc.actived && doc.simulator) || null).filter(Boolean);
const sourceSensor = getSourceSensor(dragObject);
// check simulator is empty
const sensors: SensorInterface[] = simSensors.concat(this.sensors);
const chooseSensor = (e: LocateEvent) => {
let sensor;
if (newBie && !lastLocation) {
sensor = sensors.find(s => s.sensitive && s.isEnter(e));
} else {
sensor = sensors.find(s => s.sensitive && s.inRange(e)) || lastSensor;
let sensor = sensors.find(s => s.sensorAvailable && s.isEnter(e));
if (!sensor) {
if (lastSensor) {
sensor = lastSensor;
} else if (sourceSensor) {
sensor = sourceSensor;
}
}
if (sensor !== lastSensor) {
if (lastSensor) {
lastSensor.deactive();
lastSensor.deactiveSensor();
}
lastSensor = sensor;
}
@ -338,12 +390,10 @@ class Dragon {
};
}
onDragend(func: (x: { dragTarget: DragTarget; copy: boolean }, location: Location) => any) {
onDragend(func: (x: { dragTarget: DragObject; copy: boolean }, location: Location) => any) {
this.emitter.on('dragend', func);
return () => {
this.emitter.removeListener('dragend', func);
};
}
}
export const dragon = new Dragon();

View File

@ -1,114 +0,0 @@
import Hotkey, { isFormEvent } from '../utils/hotkey';
import { getCurrentDocument, getCurrentAdaptor } from './current';
import { isShadowNode } from '../document/node/shadow-node';
import { focusing } from './focusing';
import { INode, isElementNode, insertChildren } from '../document/node';
import { activeTracker } from './active-tracker';
import clipboard from '../utils/clipboard';
export const hotkey = new Hotkey();
// hotkey binding
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const sel = doc.selection;
const topItems = sel.getTopNodes();
topItems.forEach(node => {
if (isShadowNode(node)) {
doc.removeNode(node.origin);
} else {
doc.removeNode(node);
}
});
sel.clear();
});
hotkey.bind('escape', (e: KeyboardEvent) => {
const currentFocus = focusing.current;
if (isFormEvent(e) || !currentFocus) {
return;
}
e.preventDefault();
currentFocus.esc();
});
function isHTMLTag(name: string) {
return /^[a-z]\w*$/.test(name);
}
function isIgnore(uri: string) {
return /^(\.|@(builtins|html|imported):)/.test(uri);
}
function generateMaps(node: INode | INode[], maps: any = {}) {
if (Array.isArray(node)) {
node.forEach(n => generateMaps(n, maps));
return maps;
}
if (isElementNode(node)) {
const { uri, tagName } = node;
if (uri && !isHTMLTag(tagName) && !isIgnore(uri)) {
maps[tagName] = uri;
}
generateMaps(node.children, maps);
}
return maps;
}
// command + c copy command + x cut
hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc || !(focusing.id === 'outline' || focusing.id === 'canvas')) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) return;
const maps = generateMaps(selected);
const nodesData = selected.map(item => item.nodeData);
const code = getCurrentAdaptor().viewDataToSource({
file: '',
children: nodesData as any,
});
clipboard.setData({ code, maps });
/*
const cutMode = action.indexOf('x') > 0;
if (cutMode) {
const parentNode = selected.getParent();
parentNode.select();
selected.remove();
}
*/
});
// command + v paste
hotkey.bind(['command+v', 'ctrl+v'], e => {
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc) {
return;
}
clipboard.waitPasteData(e, data => {
if (data.code && data.maps) {
const adaptor = getCurrentAdaptor();
let nodesData = adaptor.parseToViewData(data.code, data.maps).children;
nodesData = doc.processDocumentData(nodesData, data.maps);
const { target, index } = doc.getSuitableInsertion();
const nodes = insertChildren(target, nodesData, index);
if (nodes) {
doc.selection.selectAll(nodes.map(o => o.id));
setTimeout(() => activeTracker.track(nodes[0]), 10);
}
}
});
});
hotkey.mount(window);

View File

@ -76,4 +76,7 @@ export default class Project {
/**
*
*/
// 通知标记删除,需要告知服务端
// 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁,
// 哪个删除就
}

View File

@ -1,4 +1,9 @@
export interface SimulatorInterface<P = object> {
import { NpmInfo } from './schema';
import { ComponentClass as ReactComponentClass, Component } from 'react';
import { LocateEvent, SensorInterface } from './dragon';
import { Point } from './document/location';
export interface SimulatorInterface<P = object> extends SensorInterface {
/**
*
*/
@ -52,15 +57,35 @@ export interface SimulatorInterface<P = object> {
/**
*
*/
scrollToNode(node: INode, detail?: any): void;
scrollToNode(node: Node, detail?: any): void;
/**
* event canvasX, globalX
*/
fixEvent(e: LocateEvent): LocateEvent;
getComponent(npmInfo: object): ReactComponent | any;
getViewInstance(node: Node): ViewInstance[] | null;
/**
*
*/
toLocalPoint(point: Point): Point;
/**
*
*/
toGlobalPoint(point: Point): Point;
/**
*
*/
getComponent(npmInfo: NpmInfo): ComponentClass | any;
/**
*
*/
getComponentInstance(node: Node): ComponentInstance[] | null;
/**
*
*/
getComponentContext(node: Node): object;
/**
*
@ -70,5 +95,14 @@ export interface SimulatorInterface<P = object> {
/**
*
*/
destroy(): void;
purge(): void;
}
/**
*
*/
export type ComponentClass = ReactComponentClass | object;
/**
*
*/
export type ComponentInstance = Element | Component<any, any> | object;