6

Twaver HTML5中的 CloudEditor 进行Angular2 重写

 2 years ago
source link: https://segmentfault.com/a/1190000040795324
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK