二次开发draw.io

发布时间:2024-09-17 10:01

准备工作

克隆代码

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
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

var insertCellStyleProperties = graph.getCurrentCellStyle(insertCell)
var insertCellId = mxUtils.getValue(insertCellStyleProperties, \'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。

定制控件交互性

业务需求:控件是不可编辑且不可调整尺寸的——
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是经过逻辑处理后的属性类型,不一定是定义时传入的类型。

  /**
   * 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;
};

删除初始化跳转页

\"二次开发draw.io_第1张图片\"

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

 

修改标题结构与样式

// 结构生成: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


  
    
    
    
      
    
    
      
    
    
      
    
    
      
    
    
      
    
  

转成对应的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执行脚本时默认了全局
\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号