Twaver HTML5 中的 CloudEditor 进行 Angular2 重写
背景
业务进度紧迫,于是破费俩天工夫对 twaver 的 CloudEditor 进行 Angular2 重写革新以实现 twaver 初始视图构造的引入;
初识 twaver
twaver 是一个商业闭源的绘图引擎工具,相似的开源产品有 mxgraph, jointjs, raphael 等;
重写起因
-
长处
- 不减少引入三方件,manageone 以后火车版本上曾经存在 twaver,可间接应用;
- 合乎业务场景,twaver 官网提供了以后开发的利用场景样例且官网样例丰盛;
- 性能稳定性已验证,公司有产品曾经应用其作出更简单场景的性能,沟通后首次判断二次开发问题不大;
- Angular2 框架兼容,twaver 的技术栈应用原生 js 实现与以后应用 Angular2 框架无缝集成;
-
毛病
- 官网 demo 中大量应用 jquery 库操作 dom,jqueryUI 库实现 UI 组件和款式,首次引入须要对这些额定的三方件性能进行剥离和剔除;
- 没有源码,不利于调试和排查问题;
- 相熟度低,以后组内没人理解 twaver;
CloudEditor 主体内容:
|-- CloudEditor
|-- CloudEditor.html
|-- css
| |-- bootstrap.min.css
| |-- jquery-ui-1.10.4.custom.min.css
| |-- jquery.ui.all.css
| |-- images
| |-- animated-overlay.gif
|-- images
| |-- cent32os_s.png
| |-- zoomReset.png
|-- js
|-- AccordionPane.js
|-- category.js
|-- editor.js
|-- GridNetwork.js
|-- images.js
|-- jquery-ui-1.10.4.custom.js
|-- jquery.js
重写的次要准则:
- 输入文件均以 Typescript 语言实现,并减少类型申明文件;
- 剥离间接操作 dom 的操作,即移除 jquery 库;
- 改写 twaver 中过久的语法,ES6 语法革新;
左树菜单
CloudEditor 中左树菜单次要是一个手风琴成果的列表,其实现是应用 AccordionPanel.js 这个文件,其内容是应用动静拼接 dom 的形式动静生成右面板的内容;咱们应用 Angular 的模板个性,将其改写为 Angular 组件 menu,将原来 JS 操作 dom 的低效操作全副移除。
AccorditonPanel 剖析
// 这里申明了一个 editor 命名空间下的函数变量 AccordionPane
editor.AccordionPane = function() {this.init();
};
// 外部办法根本都是为了生成左树菜单构造,如下办法
createView: function() {var rootView = $('<div id="accordion-resizer"class="ui-widget-content"></div>');
this.mainPane = $('<div id="accordion"></div>');
this.setCategories(categoryJson.categories);
rootView.append(this.mainPane);
return rootView[0];
},
// 生成菜单题目
initCategoryTitle: function(title) {var titleDiv = $('<h3>' + title + '</h3>');
this.mainPane.append(titleDiv);
},
// 生成菜单内容
initCategoryContent: function(datas) {var contentDiv = $('<ul class="mn-accordion"></ul>');
for (var i = 0; i < datas.length; i++) {var data = datas[i];
contentDiv.append(this.initItemDiv(data));
}
this.mainPane.append(contentDiv);
},
// 生成菜单项
initItemDiv: function(data) {
var icon = data.icon;
var itemDiv = $('<li class="item-li"></li>');
var img = $('<img src=' + icon + '></img>');
img.attr('title', data.tooltip);
var label = $('<div class="item-label">' + data.label + '</div>');
itemDiv.append(img);
itemDiv.append(label);
this.setDragTarget(img[0], data);
return itemDiv;
},
应用 tiny 组件重写构造
<div id='left-tree-menu'>
<tp-accordionlist [options]="menuData">
<!-- 自定义面板内容 -->
<ng-template #content let-menuGroup let-i=index>
<div *ngFor="let item of menuGroup.contents" [id]="item.label" class="item"
[attr.data-type]="item.type" [attr.data-width]="item.width" [attr.data-height]="item.height"
[attr.data-os]="item.os" [attr.data-bit]="item.bit" [attr.data-version]="item.version"
[title]="item.tooltip">
<img [src]="item.icon" (dragstart)="dragStartMenuItem($event, item)"/>
<div class="item-label">{{item.label}}</div>
</div>
</ng-template>
</tp-accordionlist>
</div>
重写后组件逻辑
次要是解决数据模型与 UI 组件模型的映射关系
import {Component, Input, OnInit} from '@angular/core';
import {TpAccordionlistOption} from '@cloud/tinyplus3';
@Component({
selector: 'design-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.less']
})
export class MenuComponent implements OnInit {constructor() { }
ngOnInit(): void {}
@Input() set inputMenuData(v) {setTimeout(() => {this.menuData = this.b2uMenuData(v.categories);
});
}
menuData:TpAccordionlistOption[] = [];
categories: any[];
/**
* 设置菜单项数据
* @param categories 菜单数据列表
*/
setCategories(categories) {this.categories = categories;}
/**
* 菜单项数据转换为 UI 组件数据
* @param bData 菜单模型数据
* @returns 手风琴 UI 组件数据
*/
b2uMenuData(bData: Array<any>): Array<TpAccordionlistOption>{return bData.map((item, i) => {let tpAccordionlistOption: TpAccordionlistOption = {};
tpAccordionlistOption.disabled = false;
tpAccordionlistOption.headLabel = item.title;
tpAccordionlistOption.open = !Boolean(i);
tpAccordionlistOption.headClick = () => {};
tpAccordionlistOption.contents = [...item.contents];
tpAccordionlistOption.actionmenu = {items: []
};
return tpAccordionlistOption;
});
}
/**
* 拖拽菜单项性能
* @param event 拖拽事件
* @param data 拖拽数据
*/
dragStartMenuItem(event, data) {
data.draggable = true;
event.dataTransfer.setData("Text", JSON.stringify(data));
}
}
绘制舞台
CloudEditor 中舞台的实现是应用 GridNetwork.js 这个文件;舞台是通过扩大 twaver.vector.Network 来实现的
GridNetwork 剖析
在这个文件中,次要实现了跟舞台上相干的外围性能,拖放事件,导航窗格,简略的属性面板等
这个文件的重构须要减少大量类型申明, 以确保 ts 类型推断失常应用,在这部分,我放弃最大的克服,尽量避免应用 any 类型,对于已知的类型进行了申明增加。
缺失的类型申明
declare interface Window {
twaver: any;
GAP: number;
}
declare var GAP: number;
declare interface Document {ALLOW_KEYBOARD_INPUT: any;}
declare namespace _twaver {
export var html: any;
export class math {static createMatrix(angle, x, y);
}
}
declare namespace twaver {
export class Util {static registerImage(name: string, obj: object);
static isSharedLinks(host: any, element: any);
static moveElements(selections, xoffset, yoffset, flag: boolean);
}
export class Element {getLayerId();
getImage();
getHost();
getLayerId();
setClient(str, flag: boolean);
}
export class Node {getImage();
}
export class ElementBox {getLayerBox(): twaver.LayerBox;
add(node: twaver.Follower| twaver.Link);
getUndoManager();
addDataBoxChangeListener(fn: Function);
addDataPropertyChangeListener(fn: Function);
getSelectionModel();}
export class SerializationSettings {static getStyleType(propertyName);
static getClientType(propertyName);
static getPropertyType(propertyName);
}
export class Follower {constructor(obj: any);
setLayerId(id: string);
setHost(host: any);
setSize(w: boolean, h: boolean);
setCenterLocation(location: any);
setVisible(visible:boolean);
}
export class Property { }
export class Link {constructor(one, two);
getClient(name: string);
getFromNode();
getToNode();
setClient(attr, val);
setStyle(attr, val);
}
export class Styles {static setStyle(attr: string, val: any);
}
export class List extends Set { }
export class Layer{constructor(name: string);
}
export class LayerBox {add(box: twaver.Layer, num?: number);
}
export namespace controls {
export class PropertySheet {constructor(box: twaver.ElementBox);
getView(): HTMLElement;
setEditable(editable: boolean);
getPropertyBox();}
}
export namespace vector {
export class Overview {constructor(obj: any);
getView(): HTMLElement;}
export class Network {invalidateElementUIs();
setMovableFunction(fn:Function);
getSelectionModel();
removeSelection();
getElementBox(): twaver.ElementBox;
setKeyboardRemoveEnabled(keyboardRemoveEnabled: boolean);
setToolTipEnabled(toolTipEnable: boolean);
setTransparentSelectionEnable(transparent: boolean);
setMinZoom(zoom:number);
setMaxZoom(zoom:number);
getView();
setVisibleFunction(fn: Function);
getLabel(data: twaver.Link | { getName();});
setLinkPathFunction(fn:Function);
getInnerColor(data: twaver.Link);
adjustBounds(obj: any);
addPropertyChangeListener(fn: Function);
getElementAt(e: Event | any): twaver.Element;
setInteractions(option: any);
getLogicalPoint(e: Event | any);
getViewRect();
setViewRect(x,y,w,h);
setDefaultInteractions();
getZoom();
// 如下页面用到的公有属性,但在 api 中为申明
__button;
__startPoint;
__resizeNode;
__originSize;
__resize;
__createLink;
__fromButton;
__dragging;
__currentPoint;
__focusElement;
}
}
}
重写后的 stage.ts 文件 (本文省略了未改变代码)
export default class Stage extends twaver.vector.Network {constructor(editor) {super();
this.editor = editor;
this.element = this.editor.element;
twaver.Styles.setStyle('select.style', 'none');
twaver.Styles.setStyle('link.type', 'orthogonal');
twaver.Styles.setStyle('link.corner', 'none');
twaver.Styles.setStyle('link.pattern', [8, 8]);
this.init();}
editor;
element: HTMLElement;
box: twaver.ElementBox;
init() {this.initListener();
}
initOverview () {}
sheet;
sheetBox;
initPropertySheet () {}
getSheetBox() {return this.sheetBox;}
infoNode;
optionNode;
linkNode;
fourthNode;
initListener() {_twaver.html.addEventListener('keydown', 'handle_keydown', this.getView(), this);
_twaver.html.addEventListener('dragover', 'handle_dragover', this.getView(), this);
_twaver.html.addEventListener('drop', 'handle_drop', this.getView(), this);
_twaver.html.addEventListener('mousedown', 'handle_mousedown', this.getView(), this);
_twaver.html.addEventListener('mousemove', 'handle_mousemove', this.getView(), this);
_twaver.html.addEventListener('mouseup', 'handle_mouseup', this.getView(), this);
//...
}
refreshButtonNodeLocation (node) {var rect = node.getRect();
this.infoNode.setCenterLocation({x: rect.x, y: rect.y});
this.optionNode.setCenterLocation({x: rect.x, y: rect.y + rect.height});
this.linkNode.setCenterLocation({x: rect.x + rect.width, y: rect.y});
this.fourthNode.setCenterLocation({x: rect.x + rect.width, y: rect.y + rect.height});
}
handle_mousedown(e) { }
handle_mousemove(e) { }
handle_mouseup(e) { }
handle_keydown(e) { }
//get element by mouse event, set lastElement as ImageShapeNode
handle_dragover(e) { }
handle_drop(e) { }
_moveSelectionElements(type) { }
isCurveLine () {return this._curveLine;}
setCurveLine (value) {
this._curveLine = value;
this.invalidateElementUIs();}
isShowLine () {return this._showLine;}
setShowLine (value) {
this._showLine = value;
this.invalidateElementUIs();}
isLineTip () {return this._lineTip;}
setLineTip (value) {
this._lineTip = value;
this.invalidateElementUIs();}
paintTop (g) { }
paintBottom(g) {}}
主入口控制器
CloudEditor 中入口控制器应用 editor.js 实现,我这里为了集成到 angular 我的项目中减少了 twaver.component.ts 组件,用来疏导 editor 的引入和实例化。
第一局部 twaver 组件文件
模板局部
<div id="toolbar">
<button *ngFor="let toolItem of toolbarData" [id]="toolItem.id" [title]="toolItem.title">
<img [src]="toolItem.src"/>
</button>
</div>
<div class="main">
<div class="editor-container">
<design-menu [inputMenuData]="menuData"></design-menu>
<div class="stage" id="stage">
</div>
</div>
</div>
逻辑局部
import {Component, OnInit, ElementRef, NgZone, AfterViewInit} from '@angular/core';
import * as twaver from "../../../lib/twaver.js";
import "./shapeDefined";
import TwaverEditor from "./twaver-editor";
import {menuData, toolbarData} from './editorData';
window.GAP = 10;
@Component({
selector: 'design-twaver',
templateUrl: './twaver.component.html',
styleUrls: ['./twaver.component.less']
})
export class TwaverComponent implements OnInit, AfterViewInit {constructor(private element: ElementRef, private zone: NgZone) { }
twaverEditor: TwaverEditor;
menuData = {categories: []
};
toolbarData = toolbarData;
ngOnInit(): void {}
ngAfterViewInit() {this.twaverEditor = new TwaverEditor(this.element.nativeElement);
this.menuData = menuData;
}
}
第二局部 TwaverEditor 文件
这个文件是 editor.js 的主体局部重写后的文件 (省略未改变内容,只保留构造)。
import Stage from './stage';
export default class TwaverEditor {constructor(element) {
this.element = element;
this.init()}
element;
stage: Stage;
init() {this.stage = new Stage(this);
let stageDom = this.element.querySelector('#stage');
stageDom.append(this.stage.getView());
this.stage.initOverview();
this.stage.initPropertySheet();
this.adjustBounds();
this.initProperties();
// this.toolbar = new Toolbar();
window.onresize = (e) => {this.adjustBounds();
};
}
adjustBounds() {let stageDom = this.element.querySelector('#stage');
this.stage.adjustBounds({
x: 0,
y: 0,
width: stageDom.clientWidth,
height: stageDom.clientHeight
});
}
initProperties() {}
isFullScreenSupported () {}
toggleFullscreen() {}
getAngle (p1, p2) { }
fixNodeLocation (node) { }
layerIndex = 0;
addNode (box, obj, centerLocation, host) { }
GAP = 10;
fixLocation (location, viewRect?) { }
fixSize (size) { }
addStyleProperty (box, propertyName, category, name) {return this._addProperty(box, propertyName, category, name, 'style');
}
addClientProperty (box, propertyName, category, name) {return this._addProperty(box, propertyName, category, name, 'client');
}
addAccessorProperty (box, propertyName, category, name) {return this._addProperty(box, propertyName, category, name, 'accessor');
}
_addProperty (box, propertyName, category, name, proprtyType) {}}
输入清单
实现次要输入内容:
- 实现 Typescript 须要的类型申明文件,即 twaver.d.ts 文件
- 实现左树菜单的性能,即 menu 组件文件;
- 实现绘制操作舞台性能,即 stage.ts 文件;
- 实现编辑器主控制器,即 TwaverEditor.ts 文件
|-- twaver
|-- editorData.ts # 数据文件,蕴含左树列表数据
|-- shapeDefined.ts # 图形绘制定义
|-- stage.ts # 舞台类
|-- twaver-editor.ts # twaver 主入口控制器
|-- twaver.component.html
|-- twaver.component.less
|-- twaver.component.ts # twaver Angular 组件
|-- twaver.module.ts # twaver Module
|-- menu # meun 组件
|-- menu.component.html
|-- menu.component.less
|-- menu.component.ts
总结
重写 CloudEditor 只是一段旅途的开始,心愿此文能帮忙小伙伴们开个好头,大家能够顺利了解 twaver 中的一些 api 和语法。