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和语法。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。