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命名空间下的函数变量AccordionPaneeditor.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和语法。