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和語法。