Twaver HTML5中的 CloudEditor 进行Angular2 重写

Twaver HTML5中的 CloudEditor 进行Angular2 重写
背景 业务进度紧迫,于是花费俩天时间对 twaver 的 CloudEditor 进行Angular2 重写改造以实现twaver初始视图结构的引入;
初识twaver twaver是一个商业闭源的绘图引擎工具, 类似的开源产品有 mxgraph, jointjs, raphael等;
重写原因

  • 【Twaver HTML5中的 CloudEditor 进行Angular2 重写】优点
    • 不增加引入三方件,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 = https://www.it610.com/article/datas[i]; contentDiv.append(this.initItemDiv(data)); } this.mainPane.append(contentDiv); }, // 生成菜单项 initItemDiv: function(data) { var icon = data.icon; var itemDiv = $('
  • '); var img = $('Twaver HTML5中的 CloudEditor 进行Angular2 重写'); img.attr('title', data.tooltip); var label = $('' + data.label + ''); itemDiv.append(img); itemDiv.append(label); this.setDragTarget(img[0], data); return itemDiv; },

    使用tiny组件重写结构
    Twaver HTML5中的 CloudEditor 进行Angular2 重写 {{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 = https://www.it610.com/article/{ 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和语法。

      推荐阅读