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