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 = $('
'); this.mainPane = $('
'); this.setCategories(categoryJson.categories); rootView.append(this.mainPane); return rootView[0]; }, // 生成菜单标题 initCategoryTitle: function(title) { var titleDiv = $('

' + title + '

'); this.mainPane.append(titleDiv); }, // 生成菜单内容 initCategoryContent: function(datas) { var contentDiv = $('
    '); 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 = $('
  • '); var img = $(''); img.attr('title', data.tooltip); var label = $('
    ' + data.label + '
    '); itemDiv.append(img); itemDiv.append(label); this.setDragTarget(img[0], data); return itemDiv; },

    使用tiny组件重写结构

    {{item.label}}

    重写后组件逻辑

    主要是处理数据模型与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): Array{ 
        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组件文件

    模板部分

    逻辑部分

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

    你可能感兴趣的:(editor)