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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理