乐趣区

关于editor:Twaver-HTML5中的-CloudEditor-进行Angular2-重写

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

退出移动版