美文网首页
二次开发draw.io

二次开发draw.io

作者: 说叁两事 | 来源:发表于2022-05-31 11:20 被阅读0次

    准备工作

    克隆代码

    github#draw.io切换需要的Tag进行下载,当前以v17.4.3为示例。

    本地运行

    1. 安装browser-sync或其它本地服务器工具
    2. 解压drawio-X.zip压缩包,使用IDE打开
    3. browser-sync start --server ./src/main/webapp --files .运行本地3000端口启动服务
    4. 浏览器访问localhost:3000 即可

    开启调试模式

    ./src/main/webapp/index.html源码可见,通过URL参数?dev=1开启调试模式。

    Notes:开启调试模式后,个别静态资源请求会报错 —— 根据报错域名devhost.jgraph.com查找对应资源,修改访问地址:

     if (urlParams['dev'] == '1') {
      // Used to request grapheditor/mxgraph sources in dev mode
      // var mxDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main';
      var mxDevUrl = './';
    
      // Used to request draw.io sources in dev mode
      // var drawDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main/webapp/';
      var drawDevUrl = './';
    
    

    URL Query String

    参数名 参数值 说明
    dev 1 1: 开启调试模式
    local 1 1: 只能本地存储
    sync 'manual' --
    appLang -- --
    lang 'en' 、 'zh' ... 设置界面语言
    mode 'dropbox' 、'trello'、'google' --
    splash '0' --
    title null 文件名
    create -- --
    loc -- --
    lightbox '1' --
    embed '1' --
    libs '1' --
    embed 'aws4' 、 'aws3' --
    offline '0' 、 '1' 是否离线存储
    chrome '0' 、 '1' --
    stealth '0' 、 '1' --
    embedRT '0' 、 '1' --
    rt '0' 、 '1' --
    fast-sync ''0' 、 '1' --
    plugins '0' 、 '1' --
    db '0' 、 '1' --
    test '0' 、 '1' --
    od '0' 、 '1' --
    tr '0' 、 '1' --
    extAuth '0' 、 '1' --
    open null 是否启动时直接打开标签页
    atlas '0' 、 '1' --
    drive '0' 、 '1' --
    url null --
    nowarn '0' 、 '1' --
    desc -- --
    data -- --
    browser 0' 、 '1' --
    notitle 0' 、 '1' --
    noLangIcon 0' 、 '1' --
    sketch 'device' --
    sockets '0' 、 '1' --
    lockdown '0' 、 '1' --
    ignoremime -- --
    thumb '0' 、 '1' --
    gPickerSize -- --
    thumb '0' 、 '1' --
    pwa '0' 、 '1' --
    safe-style-src '0' 、 '1' --
    page -- --
    sb '0' 、 '1' --
    pv '0' 、 '1' --
    edge 'move' --
    viewer -- --
    format '0' 、 '1' --
    page-id -- --
    rough -- --
    format '0' 、 '1' --
    modified '0' 、 '1' --
    saveAndExit null --
    noSaveBtn null --
    noExitBtn null --
    proto 'json' --
    embedInline '0' 、 '1' --
    publishClose '0' 、 '1' --
    demo '0' 、 '1' --
    forceMigration -- --
    publishClose '0' 、 '1' --
    configure '0' 、 '1' --
    ui null 、'sketch' 、'dark' 、 'atlas' 、 'min' 修改画布主题,用于与响应式布局.
    默认:大屏
    ‘sketch':小屏
    'dark'::深色模式
    'atlas':蓝色主题
    'min':工具栏浮层展示
    sidebar-entries undefined 、 'large' 设置侧边栏控件缩略图尺寸
    默认:32px
    large: 42px
    export -- --
    gitlab -- --
    gitlab-id -- --
    newTempDlg '0' 、 '1' --
    keepmodified '0' 、 '1' --
    enableSpellCheck '0' 、 '1' --
    winCtrls '0' 、 '1' --
    libraries '0' 、 '1' --
    search-shapes null --
    clibs null --
    ownerEml null --
    odAuthCancellable '0' 、 '1' --
    no-p2p '0' 、 '1' --
    grid '0' 、 '1' --
    nav '0' 、 '1' --
    hide-pages '0' 、 '1' --
    border -- --
    highlight -- --
    touch -- --
    filesupport '0' 、 '1' --
    translate-diagram '0' 、 '1' --
    diagram-language '0' 、 '1' --
    zoom 'nocss' --
    replay-data -- --
    delay-delay -- --
    orgChartDev '0' 、 '1' --

    以上参数可通过urlParams['delay-delay']获取。

    🙇Web Javascript注入顺序

    // dev: src\main\webapp\index.html
    ...
    var mxDevUrl = './';
    var drawDevUrl = './';
    var geBasePath = drawDevUrl + '/js/grapheditor';
    var mxBasePath = mxDevUrl + '/mxgraph';
    ...
    mxscript(drawDevUrl + 'js/PreConfig.js'); // 全局配置
    mxscript(drawDevUrl + 'js/diagramly/Init.js'); // 依据URL Query String初始化urlParmas对象
    mxscript(geBasePath + '/Init.js'); // 初始化全局路径
    mxscript(mxBasePath + '/mxClient.js'); // 提供控件形状&文本渲染、交互的基础库
    mxscript(drawDevUrl + 'js/diagramly/Devel.js'); // 执行初始化脚本列表
    mxscript(drawDevUrl + 'js/PostConfig.js'); // 全局配置
    ...
    App.main(); // App对象在 12L mxscript(drawDevUrl + 'js/diagramly/Devel.js')执行脚本列表初始化时注入的。 —— 283L mxscript(drawDevUrl + 'js/diagramly/App.js');
    
    
    // prod:src\main\webapp\index.html
    mxscript('js/app.min.js')
    
    

    万物之初App.main()

    初始化流程

    // main {Function} [src/main/webapp/js/diagramly/App.js]
    /**
     * Program flow starts here.
     *
     * Optional callback is called with the app instance.
     */
    App.main = function(callback, createUi){
      ...
      doMain(); // 根据配置项初始化页面:主题、自动保存、字体、语言;调用同文件中的doLoad()——> 调用realMain()
    };
    
    
    // realMain {Function} [src/main/webapp/js/diagramly/App.js]
    new Editor()
    new App()
    EditorUi.call(this, ...)
        EditorUi.prototype.createDivs();
        EditorUi.prototype.createUi();
            EditorUi.prototype.createSidebar()
                new Sidebar(this, container);
                    Sidebar.prototype.init() // 这里对应着侧边栏的全部图形
                    Sidebar.prototype.initPalettes()
                    Sidebar.prototype.showEntries() // 调整entires参数即可调整面板控件的展示,且只有Sidebar.prototype.configure注册的控件,才是默认展示可见的
    //                EditorUi.container.appendChild(this.formatContainer) // 渲染侧边栏DOM,所以,要检索formatContainer的源码处理。
    //                EditorUi.prototype.createFormat()
    //                    new Format(this, container); // 交互操作逻辑
        EditorUi.prototype.refresh();
    App.prototype.load()
        Editor.graph.setEnabled()
            Editor.prototype.createGraph()
        mxGraph.setEnabled()
    App.prototype.start()
    App.prototype.restoreLibraries()
    App.prototype.loadLibraries()
    this.editor.sidebar // 侧边栏渲染成功
    
    

    Notes:

    • IndexedDB存储画布图形信息。
    • localStorage存储配置信息。

    定制化侧边栏面板

    缩减内置面板:

    1. 修改Sidebar.prototype.initPalettes()
    2. addXXXPalette()注释掉,只留下自己需要的面板即可。
    • scratchpad面板如何关闭?

    🙋增加自定义面板:

    1. 参照src\main\webapp\js\diagramly\sidebar\Sidebar-Flowchart.js,在同目录下定义新的面板函数
    • 自定义面板名

      • 需要在src\main\webapp\resources\dia.txt(i18n国际化)文件中配置好映射关系,e.g.追加metric=Metric —— 国际化,修改``src\main\webapp\resources`目录下对应的映射关系。
    • 在函数this.addPaletteFunctions(``**'metric'**``, mxResources.get(``**'metric'**``), false,...定义key值(metric)

    1. src/main/webapp/js/grapheditor/Sidebar.js中参照Sidebar.prototype.init111L,绑定新形状
    2. src\main\webapp\js\diagramly\Devel.js中参照178L,注入新函数的脚本
    3. Sidebar.prototype.initPalettes()中调用新函数即可。

    修改侧边栏底部”更多图形“的内容:

    • 缩减内置面板:缩减Sidebar.prototype.init()函数中的this.entries数据元素即可。
    • 隐藏“更多图形”:因为没有快捷方式打开,直接将sidebarFooterHeight 设置为0即可 —— 像最大化有快捷方式的,要注释相关代码。
    EditorUi.prototype.sidebarFooterHeight = 0;
    

    定制化控件形状

    🙋形状模板

    参考src\main\webapp\shapes\mxFlowchart.js文件,编程语言为svg基本语法。

    Notes:

    • 若不需要自定义形状,该项可有可无;
    • 建议与[增加自定义面板]一一对应创建;
    • 文件名为mxmetric.js自定义面板的key值

    形状属性配置

    createVertexTemplateEntry函数参数

    createVertexTemplateEntry用于生成图形信息

    参数名 默认值 说明 备注
    style -- s: value内置
    s2、s3: value外置
    - 圆角不是通过rx、ry,而是通过rounded=1定义
    - shape=step;可使用内置形状,详见src\main\webapp\js\diagramly\Editor.js 4260L
    - 形状映射/生成的逻辑见src\main\webapp\mxgraph\mxClient.js 14709L
    - - 更多style参数参见src/main/webapp/js/diagramly/Editor.js 315L、389L、427L
    width 100 -- --
    height 100 -- --
    value null 文本默认值 --
    title null 悬浮提示文本 --
    showLabel null null:侧边栏预览展示value
    false:侧边栏预览不展示value
    --
    showTitle null null:侧边栏预览支持浮层预览
    false:侧边栏预览不支持浮层预览
    --
    tags -- -- --

    源码见mxCellRenderer.prototype.createShape的定义,其中根据是否是内置形状分为不同的逻辑:

      null != a.style &&
        ((b = a.style[mxConstants.STYLE_SHAPE]),
        (b =
          null == mxCellRenderer.defaultShapes[b]
            ? mxStencilRegistry.getStencil(b)
            : null),
        (b = null != b ? new mxShape(b) : new (this.getShapeConstructor(a))()));
    

    对于侧边栏的预览图,见源码Sidebar.prototype.createThumb

    业务需求:修改控件缩略图的尺寸,修改源码src\main\webapp\js\grapheditor\Sidebar.js并URL上传参(urlParams['sidebar-entries'] === 'large')

    // src\main\webapp\js\grapheditor\Sidebar.js
    ...
    Sidebar.prototype.thumbWidth = 42; // 这里修改成需要尺寸大小,如100
    Sidebar.prototype.thumbHeight = 42; // 这里修改成需要尺寸大小,如100
    ...
    if (urlParams['sidebar-entries'] != 'large')
    {
      Sidebar.prototype.thumbPadding = (document.documentMode >= 5) ? 0 : 1;
      Sidebar.prototype.thumbBorder = 1;
      Sidebar.prototype.thumbWidth = 32;
      Sidebar.prototype.thumbHeight = 30;
      Sidebar.prototype.minThumbStrokeWidth = 1.3;
      Sidebar.prototype.thumbAntiAlias = true;
    }
    

    解析控件样式

    /**
    * 输入:mxCell实例
    * 输出:mxCell实例style属性对象 —— 该方法将style字符串转为对象
    */
    graph.getCurrentCellStyle(cell)
    

    获取控件DOM

    // Returns the DOM nodes for the given cells.
    Graph.prototype.getNodesForCells(cells) 
    

    定制控件id

    // 重写mxGraphModel.prototype.createId方法
    // 注意控件ID必须保证唯一,否则,程序崩溃
    mxGraphModel.prototype.createId = function (a) {
      var styleProperties, id;
      if (a.style) {
        styleProperties = mxUtils.parseStyle(a.style)
        id = mxUtils.getValue(styleProperties, 'id', null)
      }
      if (!id) {
        a = this.nextId;
        this.nextId++;
      }
      return id ? id : this.prefix + a + this.postfix;
    };
    // 获取style中的id
    var insertCellStyleProperties = graph.getCurrentCellStyle(insertCell)
    var insertCellId = mxUtils.getValue(insertCellStyleProperties, 'id', '')
    

    新增控件属性

    mxUtils.parseStyle = function(a) {
      const cellProperties = a.split(';').filter(item => !!item.trim()).reduce((sum, cur) => {
            const [key, value] = cur.split('=')
            sum[key.trim()] = typeof value === 'string' ? value.trim() : value
            return sum
        }, {})
      return cellProperties
    }
    // 新增代码逻辑定义新增字段
    mxCell.prototype.__originId = null;
    mxCell.prototype.__getOriginId = function () {
      return this.__originId;
    };
    mxCell.prototype.__setOriginId = function (a) {
      if (a) {
        this.__originId = a;
      }
    };
    mxCell.prototype.onInit = function () {
      var s = this.getStyle()
      if (s) {
        var o = mxUtils.parseStyle(s)
        this.__setOriginId(o.id)
      }
    }
    

    定制控件文本

    内置Shape样式详见src\main\webapp\mxgraph\mxClient.js文件格式化后的代码第5179L。

    通过this.createVertexTemplateEntry()创建的控件,文本源码详见function mxText的定义,样式见mxCellRenderer.prototype.createLabel中的a.text = new this.defaultTextShape,简单来说,通过stylefontSize可以自定义字号。

    业务需求:改控件文本fontSize,this.createVertexTemplateEntry(s + 'view;portConstraint=eastwest;cloneable=0;rotatable=0;editable=0;deletable=1;resizable=0;rounded=1;snapToPoint=1;points=[[0, 0.5],[1, 0.5]];whiteSpace=wrap;fontSize=24;size=0.5;', w, h * 0.6, '曝光', '曝光', null, null, this.getTagsForStencil(gn, 'view', dt).join(' '))中的fontSize=24;

    Sidebar.prototype.sidebarTitles = true可额外在侧边栏面板中展示控件label。

    更新控件样式

    通过原始功能判断是否有内置方法属性

    image.png

    有内置可直接调用

    // src/main/webapp/js/diagramly/Editor.js
    function createCheckbox(pName, pValue, prop)
    {
            var input = document.createElement('input');
            input.type = 'checkbox';
            input.checked = pValue == '1';
            
            mxEvent.addListener(input, 'change', function() 
            {
                    applyStyleVal(pName, input.checked? '1' : '0', prop);
            });
            return input;
    };
    ...
    // 样式的改变也会触发统一事件
    ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED],
      'values', ['0', '0'], 'cells', graph.getSelectionCells()));
          
    // 监听处理的地方src/main/webapp/js/grapheditor/EditorUi.js 710L
    this.addListener('styleChanged', mxUtils.bind(this, function(sender, evt)
    

    无内置,可参考相关逻辑自定义

    定制连线样式

    修改EdgeStyle即可

    // src/main/webapp/js/grapheditor/Graph.js 7731L
        Graph.prototype.createCurrentEdgeStyle = function()
        {
          var style = 'edgeStyle=' + (this.currentEdgeStyle['edgeStyle'] || 'none') + ';flowAnimation=1;';
          var keys = ['shape', 'curved', 'rounded', 'comic', 'sketch', 'fillWeight', 'hachureGap',
            'hachureAngle', 'jiggle', 'disableMultiStroke', 'disableMultiStrokeFill', 'fillStyle',
            'curveFitting', 'simplification', 'comicStyle', 'jumpStyle', 'jumpSize'];
    

    定位相关逻辑

    // src/main/webapp/js/grapheditor/EditorUi.js 1620L
        graph.connectVertex(temp, dir, graph.defaultEdgeLength, mouseEvent, true, true, function(x, y, execute)
        {
          execute(cell);
    
    
          if (ui.hoverIcons != null)
          {
            ui.hoverIcons.update(graph.view.getState(cell));
          }
        }, function(cells)
        {
          graph.selectCellsForConnectVertex(cells);
        }, mouseEvent, this.hoverIcons);
    // src/main/webapp/js/grapheditor/Graph.js 4300L
    Graph.prototype.connectVertex = function(sourc
    ...
        this.createCurrentEdgeStyle()
    

    定制控件交互性

    业务需求:控件是不可编辑且不可调整尺寸的——
    this.createVertexTemplateEntry(s + 'view;editable=0;resizable=0;rounded=1;whiteSpace=wrap;size=0.25;', w, h * 0.6, '曝光', '曝光', null, null, this.getTagsForStencil(gn, 'view', dt).join(' ')),中的editable=0;resizable=0;

    更多控件属性参见下边的代码。
    Notes:以下name字段是属性名,只不过这里的type是经过逻辑处理后的属性类型,不一定是定义时传入的类型。

    // src/main/webapp/js/diagramly/Editor.js 389L
      /**
       * Common properties for all edges.
       */
      Editor.commonEdgeProperties = [
            {type: 'separator'},
            {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
            {name: 'sourcePortConstraint', dispName: 'Source Constraint', type: 'enum', defVal: 'none',
              enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
            },
            {name: 'targetPortConstraint', dispName: 'Target Constraint', type: 'enum', defVal: 'none',
              enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
            },
            {name: 'jettySize', dispName: 'Jetty Size', type: 'int', min: 0, defVal: 'auto', allowAuto: true, isVisible: function(state)
            {
            return mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) == 'orthogonalEdgeStyle';
            }},
            {name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
            {name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
            {name: 'startFill', dispName: 'Start Fill', type: 'bool', defVal: true},
            {name: 'endFill', dispName: 'End Fill', type: 'bool', defVal: true},
            {name: 'perimeterSpacing', dispName: 'Terminal Spacing', type: 'float', defVal: 0},
            {name: 'anchorPointDirection', dispName: 'Anchor Direction', type: 'bool', defVal: true},
            {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
            {name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
            {name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
            {name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
            {name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
            {name: 'bendable', dispName: 'Bendable', type: 'bool', defVal: true},
            {name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
            {name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
            {name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
            {name: 'noJump', dispName: 'No Jumps', type: 'bool', defVal: false},
            {name: 'flowAnimation', dispName: 'Flow Animation', type: 'bool', defVal: false},
        {name: 'ignoreEdge', dispName: 'Ignore Edge', type: 'bool', defVal: false},
            {name: 'orthogonalLoop', dispName: 'Loop Routing', type: 'bool', defVal: false},
        {name: 'orthogonal', dispName: 'Orthogonal', type: 'bool', defVal: false}
      ].concat(Editor.commonProperties);
    
    
      /**
       * Common properties for all vertices.
       */
      Editor.commonVertexProperties = [
            {name: 'colspan', dispName: 'Colspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
            {
              var graph = format.editorUi.editor.graph;
    
    
            return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
            }},
            {name: 'rowspan', dispName: 'Rowspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
            {
              var graph = format.editorUi.editor.graph;
    
    
            return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
            }},
            {type: 'separator'},
            {name: 'resizeLastRow', dispName: 'Resize Last Row', type: 'bool', getDefaultValue: function(state, format)
            {
              var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
              var graph = format.editorUi.editor.graph;
              var style = graph.getCellStyle(cell);
    
    
              return mxUtils.getValue(style, 'resizeLastRow', '0') == '1';
            }, isVisible: function(state, format)
            {
              var graph = format.editorUi.editor.graph;
    
    
            return state.vertices.length == 1 && state.edges.length == 0 &&
              graph.isTable(state.vertices[0]);
            }},
            {name: 'resizeLast', dispName: 'Resize Last Column', type: 'bool', getDefaultValue: function(state, format)
            {
              var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
              var graph = format.editorUi.editor.graph;
              var style = graph.getCellStyle(cell);
    
    
              return mxUtils.getValue(style, 'resizeLast', '0') == '1';
            }, isVisible: function(state, format)
            {
              var graph = format.editorUi.editor.graph;
    
    
            return state.vertices.length == 1 && state.edges.length == 0 &&
              graph.isTable(state.vertices[0]);
            }},
            {name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
            {name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
            {name: 'overflow', dispName: 'Text Overflow', defVal: 'visible', type: 'enum',
              enumList: [{val: 'visible', dispName: 'Visible'}, {val: 'hidden', dispName: 'Hidden'}, {val: 'block', dispName: 'Block'},
                {val: 'fill', dispName: 'Fill'}, {val: 'width', dispName: 'Width'}]
            },
            {name: 'noLabel', dispName: 'Hide Label', type: 'bool', defVal: false},
            {name: 'labelPadding', dispName: 'Label Padding', type: 'float', defVal: 0},
            {name: 'direction', dispName: 'Direction', type: 'enum', defVal: 'east',
              enumList: [{val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
            },
            {name: 'portConstraint', dispName: 'Constraint', type: 'enum', defVal: 'none',
              enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
            },
            {name: 'portConstraintRotation', dispName: 'Rotate Constraint', type: 'bool', defVal: false},
            {name: 'connectable', dispName: 'Connectable', type: 'bool', getDefaultValue: function(state, format)
            {
              var cell = (state.vertices.length > 0 && state.edges.length == 0) ? state.vertices[0] : null;
              var graph = format.editorUi.editor.graph;
    
    
              return graph.isCellConnectable(cell);
            }, isVisible: function(state, format)
            {
            return state.vertices.length > 0 && state.edges.length == 0;
            }},
            {name: 'allowArrows', dispName: 'Allow Arrows', type: 'bool', defVal: true},
            {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
            {name: 'perimeter', dispName: 'Perimeter', defVal: 'none', type: 'enum',
              enumList: [{val: 'none', dispName: 'None'},
                  {val: 'rectanglePerimeter', dispName: 'Rectangle'}, {val: 'ellipsePerimeter', dispName: 'Ellipse'},
                  {val: 'rhombusPerimeter', dispName: 'Rhombus'}, {val: 'trianglePerimeter', dispName: 'Triangle'},
                  {val: 'hexagonPerimeter2', dispName: 'Hexagon'}, {val: 'lifelinePerimeter', dispName: 'Lifeline'},
                  {val: 'orthogonalPerimeter', dispName: 'Orthogonal'}, {val: 'backbonePerimeter', dispName: 'Backbone'},
                  {val: 'calloutPerimeter', dispName: 'Callout'}, {val: 'parallelogramPerimeter', dispName: 'Parallelogram'},
                  {val: 'trapezoidPerimeter', dispName: 'Trapezoid'}, {val: 'stepPerimeter', dispName: 'Step'},
                  {val: 'centerPerimeter', dispName: 'Center'}]
            },
            {name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
            {name: 'autosize', dispName: 'Autosize', type: 'bool', defVal: false},
            {name: 'container', dispName: 'Container', type: 'bool', defVal: false, isVisible: function(state, format)
            {
            return state.vertices.length == 1 && state.edges.length == 0;
            }},
            {name: 'dropTarget', dispName: 'Drop Target', type: 'bool', getDefaultValue: function(state, format)
            {
              var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
              var graph = format.editorUi.editor.graph;
    
    
              return cell != null && (graph.isSwimlane(cell) || graph.model.getChildCount(cell) > 0);
            }, isVisible: function(state, format)
            {
            return state.vertices.length == 1 && state.edges.length == 0;
            }},
            {name: 'collapsible', dispName: 'Collapsible', type: 'bool', getDefaultValue: function(state, format)
            {
              var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
              var graph = format.editorUi.editor.graph;
    
    
              return cell != null && ((graph.isContainer(cell) && state.style['collapsible'] != '0') ||
                (!graph.isContainer(cell) && state.style['collapsible'] == '1'));
            }, isVisible: function(state, format)
            {
            return state.vertices.length == 1 && state.edges.length == 0;
            }},
            {name: 'recursiveResize', dispName: 'Resize Children', type: 'bool', defVal: true, isVisible: function(state, format)
            {
            return state.vertices.length == 1 && state.edges.length == 0 &&
              !format.editorUi.editor.graph.isSwimlane(state.vertices[0]) &&
              mxUtils.getValue(state.style, 'childLayout', null) == null;
            }},
            {name: 'expand', dispName: 'Expand', type: 'bool', defVal: true},
            {name: 'part', dispName: 'Part', type: 'bool', defVal: false, isVisible: function(state, format)
            {
              var model = format.editorUi.editor.graph.model;
    
    
              return (state.vertices.length > 0) ? model.isVertex(model.getParent(state.vertices[0])) : false;
            }},
            {name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
            {name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
            {name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
            {name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
            {name: 'movableLabel', dispName: 'Movable Label', type: 'bool', defVal: false, isVisible: function(state, format)
            {
            var geo = (state.vertices.length > 0) ? format.editorUi.editor.graph.getCellGeometry(state.vertices[0]) : null;
    
    
            return geo != null && !geo.relative;
            }},
            {name: 'resizable', dispName: 'Resizable', type: 'bool', defVal: true},
            {name: 'resizeWidth', dispName: 'Resize Width', type: 'bool', defVal: false},
            {name: 'resizeHeight', dispName: 'Resize Height', type: 'bool', defVal: false},
            {name: 'rotatable', dispName: 'Rotatable', type: 'bool', defVal: true},
            {name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
            {name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
            {name: 'treeFolding', dispName: 'Tree Folding', type: 'bool', defVal: false},
            {name: 'treeMoving', dispName: 'Tree Moving', type: 'bool', defVal: false},
            {name: 'pointerEvents', dispName: 'Pointer Events', type: 'bool', defVal: true, isVisible: function(state, format)
            {
              var fillColor = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, null);
    
    
              return format.editorUi.editor.graph.isSwimlane(state.vertices[0]) ||
                fillColor == null || fillColor == mxConstants.NONE ||
            mxUtils.getValue(state.style, mxConstants.STYLE_FILL_OPACITY, 100) == 0 ||
            mxUtils.getValue(state.style, mxConstants.STYLE_OPACITY, 100) == 0 ||
            state.style['pointerEvents'] != null;
            }},
            {name: 'moveCells', dispName: 'Move Cells on Fold', type: 'bool', defVal: false, isVisible: function(state, format)
            {
              return state.vertices.length > 0 && format.editorUi.editor.graph.isContainer(state.vertices[0]);
            }}
      ].concat(Editor.commonProperties);
    

    定制控件浮层

    this.createVertexTemplateEntry(s + 'view;portConstraint=eastwest;cloneable=0;rotatable=0;editable=0;deletable=1;resizable=0;rounded=1;snapToPoint=1;points=[[0, 0.5],[1, 0.5]];whiteSpace=wrap;size=0.25;', w, h * 0.6, '曝光', '曝光', null, null, this.getTagsForStencil(gn, 'view', dt).join(' ')),
    

    通过portConstraint枚举值来定义控件悬浮时的箭头按钮展示。
    控件悬浮箭头部分源码见src\main\webapp\js\grapheditor\Graph.js文件中HoverIcons的定义。

    针对于浮层中的控件,源码见src\main\webapp\js\grapheditor\EditorUi.js文件中的EditorUi.prototype.getCellsForShapePicker的定义。

    ❗❗❗🚥定义控件并指定坐标插入

    // 未梳理的代码,拷贝的源码其他部分
    function updatePageLabelLocal(target, benchmark, ui, graph)
    {
      graph.setSelectionCell(benchmark);
      var result = ui.initSelectionState()
      ui.updateSelectionStateForCell(result, benchmark, [benchmark], true);
      // var rect = ui.getSelectionState();
      var rect = result;
      function formatHintText(pixels)
      {
          var unit = graph.view.unit;
          switch(unit)
          {
              case mxConstants.POINTS:
                  return pixels;
              case mxConstants.MILLIMETERS:
                  return (pixels / mxConstants.PIXELS_PER_MM).toFixed(1);
          case mxConstants.METERS:
                  return (pixels / (mxConstants.PIXELS_PER_MM * 1000)).toFixed(4);
              case mxConstants.INCHES:
                  return (pixels / mxConstants.PIXELS_PER_INCH).toFixed(2);
          }
      };
      var getUnit = function () {
        var unit = graph.view.unit;
        switch(unit)
        {
          case mxConstants.POINTS:
            return 'pt';
          case mxConstants.INCHES:
            return '"';
          case mxConstants.MILLIMETERS:
            return 'mm';
          case mxConstants.METERS:
            return 'm';
        }
      };
      var x, y
      if (rect.vertices.length == graph.getSelectionCount() && rect.x != null && rect.y != null)
      {
          x = formatHintText(rect.x)  + ((rect.x == '') ? '' : ' ' + getUnit());
          y = formatHintText(rect.y) + ((rect.y == '') ? '' : ' ' + getUnit());
      }
      var direction = ['x', 'y']
      new Array(x, y).forEach((input, idx) => {
        if (input != '')
        {
          var value = parseFloat(input);
          try
          {
            var cells = [target];
    
    
            for (var i = 0; i < cells.length; i++)
            {
              if (graph.getModel().isVertex(cells[i]))
              {
                var geo = graph.getCellGeometry(cells[i]);
    
    
                if (geo != null)
                {
                  geo = geo.clone();
    
    
                  if (geo.relative)
                  {
                    geo.offset[direction[idx]] = direction[idx] === 'x' ? value + 24 : value - 40 ;
                  }
                  else
                  {
                    geo[direction[idx]] = direction[idx] === 'x' ? value + 24 : value - 40 ;
                  }
                  var state = graph.view.getState(cells[i]);
    
    
                  if (state != null && graph.isRecursiveVertexResize(state))
                  {
                    graph.resizeChildCells(cells[i], geo);
                  }
    
    
                  graph.getModel().setGeometry(cells[i], geo);
                  graph.constrainChildCells(cells[i]);
                }
              }
            }
          }
          finally
          {
          }
        }
      })
    };
    // 插入
    Sidebar.prototype.__updatePageIndicator = function(benchmark, indicator)
    {
      var graph = this.editorUi.editor.graph;
      graph.container.focus();
      var styleProperties = graph.getCurrentCellStyle(benchmark)
      var pageId = mxUtils.getValue(styleProperties, 'id', '')
      if (pageId) {
        var cells = graph.getModel().cells
        var pageDescriptor = pageDimensionMock.find(item => item.id === pageId)
        var pageIdicatorDescriptor = pageDescriptor.indicator && pageDescriptor.indicator[indicator]
        var labelId = 'label_for_' + pageId
        var labelCell = cells[labelId]
        var label = pageIdicatorDescriptor
          ? pageIdicatorDescriptor.name + ':' + pageIdicatorDescriptor.value
          : ''
        if (labelCell) {
          graph.labelChanged(labelCell, label, false)
        } else {
          var target = graph.createVertex(
            null,
            labelId,
            label || '',
            0,
            0,
            120,
            40,
            [
              'text;',
              'html=1;',
              'align=center;',
              'verticalAlign=middle;',
              'resizable=0;',
              'cloneable=0;',
              'rotatable=0;',
              'editable=0;',
              'deletable=1;',
              'points=[];',
              'autosize=1;',
              'strokeColor=none;',
              'fillColor=none;'
            ].join(''),
            false
          );
          graph.getModel().beginUpdate();
          graph.addCell(target);
          graph.fireEvent(new mxEventObject('cellsInserted', 'cells', [target]));
          // graph.getModel().beginUpdate();
    
    
          // var tr = graph.view.translate;
          // var s = graph.view.scale;
          var pt = benchmark.geometry;
          // var node = graph.getNodesForCells([benchmark]) || []
          // // var pos = mxUtils.convertPoint(graph.container, benchmark.geometry.x, benchmark.geometry.y);
          // var pos = (node[0] && node[0].getBoundingClientRect()) || {}
    
    
            // TODO: 定位
            // target.geometry.x = pt.x / s - tr.x - target.geometry.width / 2;
            // target.geometry.y = pt.y / s - tr.y - target.geometry.height / 2;
          graph.getModel().endUpdate();
    
    
          graph.getModel().beginUpdate();
          updatePageLabelLocal(target, benchmark, this.editorUi, graph)
          graph.getModel().endUpdate();
        }
      }
    };
    

    Editor Configure

    传递形式hash参数,以_CONFIG_为前缀,示例内容:

    http://localhost:3000/?dev=1&lang=zh&ui=dark&sidebar-entries=large#_CONFIG_JTdCJTIyc2lkZWJhclRpdGxlcyUyMiUzQXRydWUlN0Q=
    

    配置文件的值需要经过JSON.stringify()、encodeURIComponent()

    😤事件绑定

    核心逻辑

    // src\main\webapp\js\grapheditor\EditorUi.js核心代码
    ...
    this.actions = new Actions(this);
    ...
    keyHandler.bindAction = mxUtils.bind(this, function(code, control, key, shift)
      {
        var action = this.actions.get(key);
    
    
        if (action != null)
        {
          var f = function()
          {
            if (action.isEnabled())
            {
              action.funct();
            }
          };
     ...
    EditorUi.prototype.createKeyHandler = function(editor) {
      ...
      var keyHandler = new mxKeyHandler(graph);
      ...
        keyHandler.bindAction(107, true, 'zoomIn'); // Ctrl+Plus
        keyHandler.bindAction(109, true, 'zoomOut'); // Ctrl+Minus
        keyHandler.bindAction(80, true, 'print'); // Ctrl+P
        keyHandler.bindAction(79, true, 'outline', true); // Ctrl+Shift+O
        ...
    

    事件触发

    this.editorUi.actions.get('print').funct()
    

    事件回调

    源码中,大部分事件都会抛出一个自定义事件,在业务逻辑上监听该自定义事件,即可写事件回调。

    下面以自定义删除事件的回调为例:

    //事件监听
      /**
       * 锁定元素
       */
      var lockCells = Object.values(graph.getModel().cells)
      graph.setSelectionCells(lockCells)
      var lockUnlockAction = this.editorUi.actions.get('lockUnlock').funct
      var deleteAllAction = this.editorUi.actions.get('deleteAll').funct
      lockUnlockAction()
      graph.clearSelection()
      graph.getModel().endUpdate();
      function clearLabel (graph, evt) {
        var eventName = evt.name
        if (eventName !== 'removeCells') return
        var eventTarget = evt.properties.cells[0]
        /**
         * 拖动Cell时,也会触发removeCells事件,此时eventTarget === undefined
         * 删除Cell时,触发removeCells事件,此时eventTarget为删除的Cell实例
         */
        if (!eventTarget) return
        var deleteCellStyleProperties = graph.getCurrentCellStyle(eventTarget)
        if (deleteCellStyleProperties.shape === 'label') return
        /**
         * 该逻辑会走两次
         * 第一次:删除当前Cell触发
         * 第二次:该逻辑中deleteAllAction()调用引发的触发,第二次需要忽略
         */
        currentInstalledIndicator = null
        graph.getModel().beginUpdate();
        graph.setSelectionCells(Object.values(graph.getModel().cells))
        lockUnlockAction()
        graph.clearSelection()
        graph.getModel().endUpdate();
        setTimeout(() => {
          /**
           * 该逻辑要延时执行,因为lockUnlockAction的状态无法同步更新的view.state中,倒是deleteAllAction中的是否可删除判断逻辑无法执行后续
           */
          try {
            graph.getModel().beginUpdate();
            graph.setSelectionCells(cellLabels)
            deleteAllAction()
            edges.forEach(edge => {
              graph.labelChanged(edge, '', false)
            })
          } catch {
    
    
          } finally {
            graph.removeListener(clearLabel)
            graph.getModel().endUpdate();
          }
        }, 4)
      }
      graph.addListener(mxEvent.REMOVE_CELLS, clearLabel)
    // 源码中抛出事件的代码src/main/webapp/mxgraph/mxClient.js
    mxGraph.prototype.removeCells = function (a, b) {
      b = null != b ? b : !0;
      null == a && (a = this.getDeletableCells(this.getSelectionCells()));
      if (b) a = this.getDeletableCells(this.addAllEdges(a));
      else {
        a = a.slice();
        for (
          var c = this.getDeletableCells(this.getAllEdges(a)),
            d = new mxDictionary(),
            e = 0;
          e < a.length;
          e++
        )
          d.put(a[e], !0);
        for (e = 0; e < c.length; e++)
          null != this.view.getState(c[e]) ||
            d.get(c[e]) ||
            (d.put(c[e], !0), a.push(c[e]));
      }
      this.model.beginUpdate();
      try {
        this.cellsRemoved(a),
          this.fireEvent(
            new mxEventObject(mxEvent.REMOVE_CELLS, 'cells', a, 'includeEdges', b)
          );
      } finally {
        this.model.endUpdate();
      }
      return a;
    };
    

    事件禁用

    // src\main\webapp\js\grapheditor\Actions.js
    Action.prototype.setEnabled = function(value)
    {
      if (this.enabled != value)
      {
        this.enabled = value;
        this.fireEvent(new mxEventObject('stateChanged'));
      }
    };
    

    比如,Delete、Backspace的删除键在只选择一个控件时,是被禁用的

    // src\main\webapp\js\grapheditor\EditorUi.js
    var actions = ['cut', 'copy', 'bold', 'italic', 'underline', 'delete', 'duplicate',
                     'editStyle', 'editTooltip', 'editLink', 'backgroundColor', 'borderColor',
                     'edit', 'toFront', 'toBack', 'solid', 'dashed', 'pasteSize',
                     'dotted', 'fillColor', 'gradientColor', 'shadow', 'fontColor',
                     'formattedText', 'rounded', 'toggleRounded', 'strokeColor',
               'sharp', 'snapToGrid'];
    
    
      for (var i = 0; i < actions.length; i++)
      {
        this.actions.get(actions[i]).setEnabled(ss.cells.length > 0);
      }
    

    Delete无法对单控件使用

    在上述代码中,事件名删除delete即可。

    侧边栏的事件

    缩略图拖拽事件

    // src/main/webapp/js/grapheditor/Sidebar.js
    // 初始化侧边栏面板后,首次展开Palette时进行绑定,调用的函数顺序
    Sidebar.prototype.createItem = 
    ...
    Sidebar.prototype.createDropHandler = 
    ....
    Sidebar.prototype.createDragPreview = 
    ...
    Sidebar.prototype.isDropStyleEnabled = 
    

    缩略图点击事件

    // src/main/webapp/js/diagramly/sidebar/Sidebar.js
    // 该事件覆盖了src/main/webapp/js/grapheditor/Sidebar.js中的对应事件
    Sidebar.prototype.itemClicked = function(cells, ds, evt) {
    ...
    

    其他对象事件触发

    graph.addListener('cellsInserted', function(sender, evt)
    {
      insertHandler(evt.getProperty('cells'), null, null, null, null, true, true);
    });
    ...
    graph.fireEvent(new mxEventObject('cellsInserted', 'cells', select));
    

    调用弹窗

    // Dialog确认框
    var popup = new TextareaDialog(this.editorUi, 'baidu.com', '1313123', mxUtils.bind(this, function () {
      his.hideDialog()
      showSecondDialog()
    }))
    this.editorUi.showDialog(popup.container, 300, 200)
    // 自定义弹窗
    var popup = new CustomDialog(
      this.editorUi,
      document.createTextNode('baidu.com'),
      mxUtils.bind(this, function () {
        console.log('----ok----')
      }),
      mxUtils.bind(this, function () {
        console.log('----cancel----')
      })
    )
    this.editorUi.showDialog(popup.container, 300, 200)
    // 内置Confirm
    EditorUi.prototype.confirm = function(msg, okFn, cancelFn)
    {
      if (mxUtils.confirm(msg))
      {
        if (okFn != null)
        {
          okFn();
        }
      }
      else if (cancelFn != null)
      {
        cancelFn();
      }
    };
    this.editorUi.confirm(
      '确认么', 
      function() {
        // ok
      }
    )
    
    // Error提示框
      this.editorUi.showError(mxResources.get('error'), mxResources.get('notInOffline'));
      // showAlert(message)
      // showError(title, message)
      //showSplash
      //showWarning
    

    节点事件调用

    添加节点

      graph.fireEvent(new mxEventObject('cellsInserted', 'cells', select));
    

    删除节点

    // 独立节点,没有连接线
    graph.deleteCells([insertCell], false)
    

    添加/修改文本

    // 以连线文本为例,lineCell.contructor === mxCell,与事件无关
    graph.labelChanged(lineCell, 'TEST', false)
    // 针对事件target、client X| Y插入文本,与事件对象挂钩
    graph.insertTextForEvent(evt, cell)
    // 底层mxcell设置文本
    mxCell.getValue()
    mxCell.setValue(newV)
    mxCell.valueChange: (newV) => oldV
    

    添加/修改事件监听

    Graph.prototype.cellLabelChanged = 
    ....
    

    国际化语言支持

    window.mxLanguageMap = window.mxLanguageMap ||修改该项可以调整右上角的语言选项,通过URL Query String配置lang可以指定某一语言。

    默认语言设置

    方案一:全局变量

    // src\main\webapp\index.html
    var drawDevUrl = './';
    var geBasePath = drawDevUrl + '/js/grapheditor';
    var mxBasePath = mxDevUrl + '/mxgraph';
    var mxLanguage = 'zh';
    

    方案二:参数对象默认值

    // src\main\webapp\js\PreConfig.js
    window.EXPORT_URL = 'REPLACE_WITH_YOUR_IMAGE_SERVER';
    window.PLANT_URL = 'REPLACE_WITH_YOUR_PLANTUML_SERVER';
    window.DRAWIO_BASE_URL = null; // Replace with path to base of deployment, e.g. https://www.example.com/folder
    window.DRAWIO_VIEWER_URL = null; // Replace your path to the viewer js, e.g. https://www.example.com/js/viewer.min.js
    window.DRAWIO_LIGHTBOX_URL = null; // Replace with your lightbox URL, eg. https://www.example.com
    window.DRAW_MATH_URL = 'math';
    window.DRAWIO_CONFIG = null; // Replace with your custom draw.io configurations. For more details, https://www.diagrams.net/doc/faq/configure-diagram-editor
    urlParams['sync'] = 'manual';
    urlParams['lang'] = 'zh';
    

    隐藏语言切换按钮

    需注释掉相关代码块

    // src\main\webapp\js\diagramly\Menus.js
    Menus.prototype.createMenubar = function(container)
          {
            var menubar = menusCreateMenuBar.apply(this, arguments);
    
    
            if (menubar != null && urlParams['noLangIcon'] != '1')
            {
              var langMenu = this.get('language');
    
    
              // if (langMenu != null)
              // {
              //  var elt = menubar.addMenu('', langMenu.funct);
              //  elt.setAttribute('title', mxResources.get('language'));
              //  elt.style.width = '16px';
              //  elt.style.paddingTop = '2px';
              //  elt.style.paddingLeft = '4px';
              //  elt.style.zIndex = '1';
              //  elt.style.position = 'absolute';
              //  elt.style.display = 'block';
              //  elt.style.cursor = 'pointer';
              //  elt.style.right = '17px';
    
    
              //  if (uiTheme == 'atlas')
              //  {
              //    elt.style.top = '6px';
              //    elt.style.right = '15px';
              //  }
              //  else if (uiTheme == 'min')
              //  {
              //    elt.style.top = '2px';
              //  }
              //  else
              //  {
              //    elt.style.top = '0px';
              //  }
    
    
              //  var icon = document.createElement('div');
              //  icon.style.backgroundImage = 'url(' + Editor.globeImage + ')';
              //  icon.style.backgroundPosition = 'center center';
              //  icon.style.backgroundRepeat = 'no-repeat';
              //  icon.style.backgroundSize = '19px 19px';
              //  icon.style.position = 'absolute';
              //  icon.style.height = '19px';
              //  icon.style.width = '19px';
              //  icon.style.marginTop = '2px';
              //  icon.style.zIndex = '1';
              //  elt.appendChild(icon);
              //  mxUtils.setOpacity(elt, 40);
    
    
              //  if (urlParams['winCtrls'] == '1')
              //  {
              //    elt.style.right = '95px';
              //    elt.style.width = '19px';
              //    elt.style.height = '19px';
              //    elt.style.webkitAppRegion = 'no-drag';
              //    icon.style.webkitAppRegion = 'no-drag';
              //  }
    
    
              //  if (uiTheme == 'atlas' || uiTheme == 'dark')
              //  {
              //    elt.style.opacity = '0.85';
              //    elt.style.filter = 'invert(100%)';
              //  }
    
    
              //  document.body.appendChild(elt);
              //  menubar.langIcon = elt;
              // }
            }
    
    
            return menubar;
          };
        }
    

    其它

    修改文件名

    // js修改文件名
    this.editorUi.getCurrentFile().rename('234324')
    // 默认文件名 
    // src/main/webapp/js/diagramly/EditorUi.js   
    this.defaultFilename = mxResources.get('untitledDiagram');
    
    

    禁止修改文件名

    // src/main/webapp/js/diagramly/LocalFile.js
    // isRenamable函数返回false时,无法通过交互修改文件名,能通过js修改
    LocalFile.prototype.isRenamable = function()
    {
      return false;
    };
    
    

    删除初始化跳转页

    image.png

    注释掉src/main/webapp/index.html相应的结构

     <!-- <div class="geBlock">
        <h1>Flowchart Maker and Online Diagram Software</h1>
        <p>
          diagrams.net (formerly draw.io) is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
          to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy&trade; and Lucidchart&trade; files .
        </p>
        <h2 id="geStatus">Loading...</h2>
        <p>
          Please ensure JavaScript is enabled.
        </p>
      </div> -->
    
    

    修改标题结构与样式

    // 结构生成:src/main/webapp/js/diagramly/App.js —— 6408L
    if (this.fname != null)
    {
      this.fnameWrapper.style.display = 'block';
      this.fname.innerHTML = '';
      var filename = (file.getTitle() != null) ? file.getTitle() : this.defaultFilename;
      mxUtils.write(this.fname, filename);
      this.fname.setAttribute('title', filename + ' - ' + mxResources.get('rename'));
    }
    // 容器样式 6786L
        this.fnameWrapper = document.createElement('div');
        this.fnameWrapper.style.position = 'absolute';
        this.fnameWrapper.style.right = '120px';
        this.fnameWrapper.style.left = '60px';
        this.fnameWrapper.style.top = '19px';
        this.fnameWrapper.style.height = '26px';
        this.fnameWrapper.style.display = 'none';
        this.fnameWrapper.style.overflow = 'hidden';
        this.fnameWrapper.style.textOverflow = 'ellipsis';
    
    

    隐藏便签本

    注释掉相关构造器的调用即可:

     // src/main/webapp/js/diagramly/App.js
       if (name == '.scratchpad' && xml == null)
      {
        xml = this.emptyLibraryXml;
      }
    
      if (xml != null)
      {
        onload(new StorageLibrary(this, xml, name));
      }
      else
      {
        onerror();
      }
    
    

    便签本构造器为StorageLibrary

    有三个位置调用src/main/webapp/js/diagramly/App.jssrc/main/webapp/js/diagramly/sidebar/Sidebar.jssrc/main/webapp/js/diagramly/EditorUi.js

    该侧边栏的便签本是在App.js中注册的,在逻辑中属于必插入的侧边栏资源库,和loadLibraries挂钩,相关逻辑在src/main/webapp/js/diagramly/App.js5477L~5567L

    隐藏存储选项弹窗

    解决方案:

    修改弹窗逻辑,不生成结构,直接渲染EditorUi

    // src/main/webapp/js/diagramly/Dialogs.js 258L
      else if (!mxClient.IS_CHROMEAPP && (this.mode == null || force))
      {
        // 删除弹窗逻辑
        // var rowLimit = (serviceCount == 4) ? 2 : 3;
    
        // var dlg = new StorageDialog(this, mxUtils.bind(this, function()
        // {
        //  this.hideDialog();
        //  showSecondDialog();
        // }), rowLimit);
    
        // this.showDialog(dlg.container, (rowLimit < 3) ? 200 : 300,
        //  ((serviceCount > 3) ? 320 : 210), true, false, undefined, undefined, true);
        // 新增渲染逻辑
        this.editorUi.hideDialog();
        var prev = Editor.useLocalStorage;
        this.editorUi.createFile(this.editorUi.defaultFilename,
          null, null, null, null, null, null, true);
        Editor.useLocalStorage = prev;
      }
    
    

    弹窗结构:

    // src/main/webapp/js/diagramly/Dialogs.js
    var StorageDialog = function(editorUi, fn, rowLimit)
    {
    ...
     // 稍后再决定的逻辑
      var later = document.createElement('span');
      later.style.position = 'absolute';
      later.style.cursor = 'pointer';
      later.style.bottom = '27px';
      later.style.color = 'gray';
      later.style.userSelect = 'none';
      later.style.textAlign = 'center';
      later.style.left = '50%';
      mxUtils.setPrefixedStyle(later.style, 'transform', 'translate(-50%,0)');
      mxUtils.write(later, mxResources.get('decideLater'));
      div.appendChild(later);
    
      mxEvent.addListener(later, 'click', function()
      {
        editorUi.hideDialog();
        var prev = Editor.useLocalStorage;
        editorUi.createFile(editorUi.defaultFilename,
          null, null, null, null, null, null, true);
        Editor.useLocalStorage = prev;
      });
     ...
    
    

    弹窗调用逻辑:

    // src/main/webapp/js/diagramly/App.js 3656L
    var dlg = new StorageDialog(this, mxUtils.bind(this, function()
    {
      this.hideDialog();
      showSecondDialog();
    }), rowLimit);
    
    this.showDialog(dlg.container, (rowLimit < 3) ? 200 : 300,
      ((serviceCount > 3) ? 320 : 210), true, false);
    
    
    // src/main/webapp/js/grapheditor/EditorUi.js 4618L
    EditorUi.prototype.showDialog = function( //
      ...
      this.dialog = new Dialog(this, elt, w, h, modal, closable, onClose, noScroll, transparent, onResize, ignoreBgClick);
      this.dialogs.push(this.dialog);
    }
    
    
    // src/main/webapp/js/grapheditor/Editor.js 893L
    function Dialog(editorUi, elt, w, h, modal, closable, onClose, noScroll, transparent, onResize, ignoreBgClick)
    {
    
    

    禁用画布双击快捷模版

    // src/main/webapp/js/grapheditor/EditorUi.js 1515L
    
      graph.dblClick = function(evt, cell)
      {
        if (this.isEnabled())
        {
          if (cell == null && ui.sidebar != null && !mxEvent.isShiftDown(evt) &&
            !graph.isCellLocked(graph.getDefaultParent()))
          {
            // var pt = mxUtils.convertPoint(this.container, mxEvent.getClientX(evt), mxEvent.getClientY(evt));
            // mxEvent.consume(evt);
    
            // // Asynchronous to avoid direct insert after double tap
            // window.setTimeout(mxUtils.bind(this, function()
            // {
            //  ui.showShapePicker(pt.x, pt.y);
            // }), 30);
          }
          else
          {
            graphDblClick.apply(this, arguments); // 双击编辑,禁掉就无法编辑了
          }
        }
      };
    
    

    隐藏右键菜单

    // src/main/webapp/js/grapheditor/EditorUi.js 382L
          if (mxClient.IS_IE && (typeof(document.documentMode) === 'undefined' || document.documentMode < 9))
          {
            mxEvent.addListener(this.diagramContainer, 'contextmenu', linkHandler);
          }
          else
          {
            // Allows browser context menu outside of diagram and sidebar
            this.diagramContainer.oncontextmenu = linkHandler;
          }
    
    

    隐藏Menus

    实践

    // src\main\webapp\js\grapheditor\Menus.js
    // Menus.prototype.defaultMenuItems = ['file', 'edit', 'view', 'arrange', 'extras', 'help'];
    Menus.prototype.defaultMenuItems = [];
    

    为什么

    • 不注释掉src\main\webapp\js\grapheditor\EditorUi.js中的this.menus = this.createMenus();逻辑?
    • 不注释掉src\main\webapp\js\diagramly\Devel.js中的Menus脚本?
      因为代码依赖问题,注释掉的话,需要调整其它JS脚本的逻辑。

    隐藏工具栏Toolbar

    实践

    // src\main\webapp\js\grapheditor\Toolbar.js
    function Toolbar(editorUi, container)
    {
      this.editorUi = editorUi;
      this.container = container;
      this.staticElements = [];
      // this.init();
    

    为什么

    • 不注释掉src\main\webapp\js\grapheditor\EditorUi.js中的EditorUi.prototype.createToolbar逻辑?
    • 不注释掉src\main\webapp\js\diagramly\Devel.js中的Toolbar脚本?
      因为代码依赖问题,注释掉的话,需要调整其它JS脚本的逻辑。

    隐藏配置面板Format

    实践

    控件的配置面板源码在Format.prototype.immediateRefresh的定义中,这里我们不去调用immediateRefresh函数,即不会生成各项配置面板——空面板,但,空面板宽度width还是有的,通过this.editorUi.toggleFormatPanel(false)隐藏。

    // src\main\webapp\js\grapheditor\Format.js
    Format.prototype.refresh = function()
    {
      if (this.pendingRefresh != null)
      {
        window.clearTimeout(this.pendingRefresh);
        this.pendingRefresh = null;
      }
    
    
      this.pendingRefresh = window.setTimeout(mxUtils.bind(this, function()
      {
        // this.immediateRefresh();
        this.editorUi.toggleFormatPanel(false);
      }));
    };
    

    为什么

    • 不采用以下方式隐藏——默认UI模式下可以的,min模式下会报错。
    // src\main\webapp\js\grapheditor\EditorUi.js
    EditorUi.prototype.createFormat = function(container)
    {
      // return new Format(this, container);
    };
    

    隐藏共享按钮

    需注释掉相关代码块

    // src\main\webapp\js\diagramly\App.js
        // Share
        if (urlParams['embed'] != '1' && this.getServiceName() == 'draw.io' &&
          !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
          !this.isOfflineApp())
        {
          if (file != null)
          {
            // if (this.shareButton == null)
            // {
            //  this.shareButton = document.createElement('div');
            //  this.shareButton.className = 'geBtn gePrimaryBtn';
            //  this.shareButton.style.display = 'inline-block';
            //  this.shareButton.style.backgroundColor = '#F2931E';
            //  this.shareButton.style.borderColor = '#F08705';
            //  this.shareButton.style.backgroundImage = 'none';
            //  this.shareButton.style.padding = '2px 10px 0 10px';
            //  this.shareButton.style.marginTop = '-10px';
            //  this.shareButton.style.height = '28px';
            //  this.shareButton.style.lineHeight = '28px';
            //  this.shareButton.style.minWidth = '0px';
            //  this.shareButton.style.cssFloat = 'right';
            //  this.shareButton.setAttribute('title', mxResources.get('share'));
    
    
            //  var icon = document.createElement('img');
            //  icon.setAttribute('src', this.shareImage);
            //  icon.setAttribute('align', 'absmiddle');
            //  icon.style.marginRight = '4px';
            //  icon.style.marginTop = '-3px';
            //  this.shareButton.appendChild(icon);
    
    
            //  if (!Editor.isDarkMode() && uiTheme != 'atlas')
            //  {
            //    this.shareButton.style.color = 'black';
            //    icon.style.filter = 'invert(100%)';
            //  }
    
    
            //  mxUtils.write(this.shareButton, mxResources.get('share'));
    
    
            //  mxEvent.addListener(this.shareButton, 'click', mxUtils.bind(this, function()
            //  {
            //    this.actions.get('share').funct();
            //  }));
    
    
            //  this.buttonContainer.appendChild(this.shareButton);
            // }
          }
    

    隐藏最大化、展开/折叠、Format展开...按钮

    // src\main\webapp\js\diagramly\App.js
        // this.toggleFormatElement = document.createElement('a');
        // this.toggleFormatElement.setAttribute('title', mxResources.get('formatPanel') + ' (' + Editor.ctrlKey + '+Shift+P)');
        // this.toggleFormatElement.style.position = 'absolute';
        // this.toggleFormatElement.style.display = 'inline-block';
        // this.toggleFormatElement.style.top = (uiTheme == 'atlas') ? '8px' : '6px';
        // this.toggleFormatElement.style.right = (uiTheme != 'atlas' && urlParams['embed'] != '1') ? '30px' : '10px';
        // this.toggleFormatElement.style.padding = '2px';
        // this.toggleFormatElement.style.fontSize = '14px';
        // this.toggleFormatElement.className = (uiTheme != 'atlas') ? 'geButton' : '';
        // this.toggleFormatElement.style.width = '16px';
        // this.toggleFormatElement.style.height = '16px';
        // this.toggleFormatElement.style.backgroundPosition = '50% 50%';
        // this.toggleFormatElement.style.backgroundRepeat = 'no-repeat';
        // this.toolbarContainer.appendChild(this.toggleFormatElement);
    

    按钮没有了,就把高度设置为0吧!

    // src\main\webapp\js\grapheditor\EditorUi.js
    EditorUi.prototype.toolbarHeight = 0;
    

    隐藏分页

    需注释掉相关代码块

    // src/main/webapp/js/grapheditor/EditorUi.js
    
    
    if (this.container != null && this.tabContainer != null)
    {
     // this.container.appendChild(this.tabContainer);
    }
    

    隐藏状态提醒

    // 结构生成:src/main/webapp/js/grapheditor/EditorUi.js
    EditorUi.prototype.createStatusContainer = function()
    {
      var container = document.createElement('a');
      container.className = 'geItem geStatus';
    
      return container;
    };
    // 状态设置
    EditorUi.prototype.setStatusText = function(value)
    {
      this.statusContainer.innerHTML = value;
    
      // Wraps simple status messages in a div for styling
      if (this.statusContainer.getElementsByTagName('div').length == 0)
      {
        this.statusContainer.innerHTML = '';
        var div = this.createStatusDiv(value);
        this.statusContainer.appendChild(div);
      }
    };
    // 状态监听
    this.editor.addListener('statusChanged', mxUtils.bind(this, function()
    {
      this.setStatusText(this.editor.getStatus());
    }));
    
    

    绘图结构XML

    xml

    <mxGraphModel dx="877" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
      <root>
        <mxCell id="0" />
        <mxCell id="1" parent="0" />
        <mxCell id="2x61OCl5DEtUMzRSTxGA-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="2x61OCl5DEtUMzRSTxGA-1" target="2x61OCl5DEtUMzRSTxGA-2">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="2x61OCl5DEtUMzRSTxGA-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=-0.008;entryY=0.617;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="2x61OCl5DEtUMzRSTxGA-2" target="2x61OCl5DEtUMzRSTxGA-3">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="2x61OCl5DEtUMzRSTxGA-1" value="text1" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
          <mxGeometry x="50" y="120" width="100" height="60" as="geometry" />
        </mxCell>
        <mxCell id="2x61OCl5DEtUMzRSTxGA-2" value="text2" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
          <mxGeometry x="250" y="150" width="120" height="60" as="geometry" />
        </mxCell>
        <mxCell id="2x61OCl5DEtUMzRSTxGA-3" value="text3" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
          <mxGeometry x="550" y="310" width="120" height="60" as="geometry" />
        </mxCell>
      </root>
    </mxGraphModel>
    
    

    转成对应的JSON:

    {
    
       "@dx": "877",
       "@dy": "762",
       "@grid": "1",
       "@gridSize": "10",
       "@guides": "1",
       "@tooltips": "1",
       "@connect": "1",
       "@arrows": "1",
       "@fold": "1",
       "@page": "1",
       "@pageScale": "1",
       "@pageWidth": "827",
       "@pageHeight": "1169",
       "@math": "0",
       "@shadow": "0",
       "root": [
          {
             "@id": "0"
          },
          {
             "@id": "1",
             "@parent": "0"
          },
          {
    
             "@id": "2x61OCl5DEtUMzRSTxGA-5",
             "@style": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;",
             "@edge": "1",
             "@parent": "1",
             "@source": "2x61OCl5DEtUMzRSTxGA-1",
             "@target": "2x61OCl5DEtUMzRSTxGA-2",
             "mxGeometry": {
                "@relative": "1",
                "@as": "geometry"
             }
          },
          {
             "@id": "2x61OCl5DEtUMzRSTxGA-6",
             "@style": "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=-0.008;entryY=0.617;entryDx=0;entryDy=0;entryPerimeter=0;",
             "@edge": "1",
             "@parent": "1",
             "@source": "2x61OCl5DEtUMzRSTxGA-2",
             "@target": "2x61OCl5DEtUMzRSTxGA-3",
             "mxGeometry": {
                "@relative": "1",
                "@as": "geometry"
             }
          },
          {
             "@id": "2x61OCl5DEtUMzRSTxGA-1",
             "@value": "text1",
             "@style": "rounded=1;whiteSpace=wrap;html=1;",
             "@vertex": "1",
             "@parent": "1",
             "mxGeometry": {
                "@x": "50",
                "@y": "120",
                "@width": "100",
                "@height": "60",
                "@as": "geometry"
             }
          },
          {
             "@id": "2x61OCl5DEtUMzRSTxGA-2",
             "@value": "text2",
             "@style": "rounded=1;whiteSpace=wrap;html=1;",
             "@vertex": "1",
             "@parent": "1",
             "mxGeometry": {
                "@x": "250",
                "@y": "150",
                "@width": "120",
                "@height": "60",
                "@as": "geometry"
             }
          },
          {
             "@id": "2x61OCl5DEtUMzRSTxGA-3",
             "@value": "text3",
             "@style": "rounded=1;whiteSpace=wrap;html=1;",
             "@vertex": "1",
             "@parent": "1",
             "mxGeometry": {
                "@x": "550",
                "@y": "310",
                "@width": "120",
                "@height": "60",
                "@as": "geometry"
             }
          }
       ]
    }
    
    

    打包部署

    通过链接下载打包工具Ant,以1.9.16版本为例,下载解压后,切换到解压后的目录,依次运行build.batbuild.shbootstrap.batbootstrap.sh

    切换到项目目录下,ant -file ./etc/build/build.xml,执行结束后会替换原有的线上文件。

    详情见github官方文档

    Ant命令行使用说明

    Notes:

    • 切换到解压后的目录
      • 必须执行,该脚本内的访问路径是相对路径,必须切换到解压后的目录执行脚本
    • ant -file ./etc/build/build.xml
      • Mac OS需要将ant设置为全局,win执行脚本时默认了全局

    相关文章

      网友评论

          本文标题:二次开发draw.io

          本文链接:https://www.haomeiwen.com/subject/eftyprtx.html