二次開發(fā)draw.io

準(zhǔn)備工作

克隆代碼

github#draw.io切換需要的Tag進(jìn)行下載,當(dāng)前以v17.4.3為示例薪鹦。

本地運(yùn)行

  1. 安裝browser-sync或其它本地服務(wù)器工具
  2. 解壓drawio-X.zip壓縮包,使用IDE打開
  3. browser-sync start --server ./src/main/webapp --files .運(yùn)行本地3000端口啟動(dòng)服務(wù)
  4. 瀏覽器訪問localhost:3000 即可

開啟調(diào)試模式

./src/main/webapp/index.html源碼可見裙士,通過URL參數(shù)?dev=1開啟調(diào)試模式心剥。

Notes:開啟調(diào)試模式后,個(gè)別靜態(tài)資源請求會(huì)報(bào)錯(cuò) —— 根據(jù)報(bào)錯(cuò)域名devhost.jgraph.com查找對應(yīng)資源瘩缆,修改訪問地址:

 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

參數(shù)名 參數(shù)值 說明
dev 1 1: 開啟調(diào)試模式
local 1 1: 只能本地存儲(chǔ)
sync 'manual' --
appLang -- --
lang 'en' 关拒、 'zh' ... 設(shè)置界面語言
mode 'dropbox' 、'trello'、'google' --
splash '0' --
title null 文件名
create -- --
loc -- --
lightbox '1' --
embed '1' --
libs '1' --
embed 'aws4' 着绊、 'aws3' --
offline '0' 、 '1' 是否離線存儲(chǔ)
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 是否啟動(dòng)時(shí)直接打開標(biāo)簽頁
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' 修改畫布主題,用于與響應(yīng)式布局.
默認(rèn):大屏
‘sketch':小屏
'dark'::深色模式
'atlas':藍(lán)色主題
'min':工具欄浮層展示
sidebar-entries undefined 、 'large' 設(shè)置側(cè)邊欄控件縮略圖尺寸
默認(rèn):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' --

以上參數(shù)可通過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'); // 依據(jù)URL Query String初始化urlParmas對象
mxscript(geBasePath + '/Init.js'); // 初始化全局路徑
mxscript(mxBasePath + '/mxClient.js'); // 提供控件形狀&文本渲染、交互的基礎(chǔ)庫
mxscript(drawDevUrl + 'js/diagramly/Devel.js'); // 執(zhí)行初始化腳本列表
mxscript(drawDevUrl + 'js/PostConfig.js'); // 全局配置
...
App.main(); // App對象在 12L mxscript(drawDevUrl + 'js/diagramly/Devel.js')執(zhí)行腳本列表初始化時(shí)注入的。 —— 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(); // 根據(jù)配置項(xiàng)初始化頁面:主題伐蒂、自動(dòng)保存在扰、字體、語言;調(diào)用同文件中的doLoad()——> 調(diào)用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() // 這里對應(yīng)著側(cè)邊欄的全部圖形
                Sidebar.prototype.initPalettes()
                Sidebar.prototype.showEntries() // 調(diào)整entires參數(shù)即可調(diào)整面板控件的展示存炮,且只有Sidebar.prototype.configure注冊的控件炬搭,才是默認(rèn)展示可見的
//                EditorUi.container.appendChild(this.formatContainer) // 渲染側(cè)邊欄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 // 側(cè)邊欄渲染成功

Notes:

  • IndexedDB存儲(chǔ)畫布圖形信息。
  • localStorage存儲(chǔ)配置信息寄悯。

定制化側(cè)邊欄面板

縮減內(nèi)置面板:

  1. 修改Sidebar.prototype.initPalettes()
  2. addXXXPalette()注釋掉丘薛,只留下自己需要的面板即可慕购。
  • scratchpad面板如何關(guān)閉爱致?

??增加自定義面板:

  1. 參照src\main\webapp\js\diagramly\sidebar\Sidebar-Flowchart.js判帮,在同目錄下定義新的面板函數(shù)
  • 自定義面板名

    • 需要在src\main\webapp\resources\dia.txt(i18n國際化)文件中配置好映射關(guān)系,e.g.追加metric=Metric —— 國際化,修改``src\main\webapp\resources`目錄下對應(yīng)的映射關(guān)系。
  • 在函數(shù)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础米,注入新函數(shù)的腳本
  3. Sidebar.prototype.initPalettes()中調(diào)用新函數(shù)即可准浴。

修改側(cè)邊欄底部”更多圖形“的內(nèi)容:

  • 縮減內(nèi)置面板:縮減Sidebar.prototype.init()函數(shù)中的this.entries數(shù)據(jù)元素即可宠默。
  • 隱藏“更多圖形”:因?yàn)?strong>沒有快捷方式打開敏弃,直接將sidebarFooterHeight 設(shè)置為0即可 —— 像最大化有快捷方式的隅要,要注釋相關(guān)代碼。
EditorUi.prototype.sidebarFooterHeight = 0;

定制化控件形狀

??形狀模板

參考src\main\webapp\shapes\mxFlowchart.js文件雷酪,編程語言為svg基本語法。

Notes:

  • 若不需要自定義形狀通惫,該項(xiàng)可有可無茂翔;
  • 建議與[增加自定義面板]一一對應(yīng)創(chuàng)建;
  • 文件名為mxmetric.js自定義面板的key值

形狀屬性配置

createVertexTemplateEntry函數(shù)參數(shù)

createVertexTemplateEntry用于生成圖形信息

參數(shù)名 默認(rèn)值 說明 備注
style -- s: value內(nèi)置
s2履腋、s3: value外置
- 圓角不是通過rx珊燎、ry,而是通過rounded=1定義
- shape=step;可使用內(nèi)置形狀遵湖,詳見src\main\webapp\js\diagramly\Editor.js 4260L
- 形狀映射/生成的邏輯見src\main\webapp\mxgraph\mxClient.js 14709L
- - 更多style參數(shù)參見src/main/webapp/js/diagramly/Editor.js 315L悔政、389L、427L
width 100 -- --
height 100 -- --
value null 文本默認(rèn)值 --
title null 懸浮提示文本 --
showLabel null null:側(cè)邊欄預(yù)覽展示value
false:側(cè)邊欄預(yù)覽不展示value
--
showTitle null null:側(cè)邊欄預(yù)覽支持浮層預(yù)覽
false:側(cè)邊欄預(yù)覽不支持浮層預(yù)覽
--
tags -- -- --

源碼見mxCellRenderer.prototype.createShape的定義延旧,其中根據(jù)是否是內(nèi)置形狀分為不同的邏輯:

  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))()));

對于側(cè)邊欄的預(yù)覽圖谋国,見源碼Sidebar.prototype.createThumb

業(yè)務(wù)需求:修改控件縮略圖的尺寸迁沫,修改源碼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實(shí)例
* 輸出:mxCell實(shí)例style屬性對象 —— 該方法將style字符串轉(zhuǎn)為對象
*/
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)
  }
}

定制控件文本

內(nèi)置Shape樣式詳見src\main\webapp\mxgraph\mxClient.js文件格式化后的代碼第5179L。

通過this.createVertexTemplateEntry()創(chuàng)建的控件牡整,文本源碼詳見function mxText的定義,樣式見mxCellRenderer.prototype.createLabel中的a.text = new this.defaultTextShape溺拱,簡單來說逃贝,通過stylefontSize可以自定義字號。

業(yè)務(wù)需求:改控件文本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可額外在側(cè)邊欄面板中展示控件label沐扳。

更新控件樣式

通過原始功能判斷是否有內(nèi)置方法屬性

image.png

有內(nèi)置可直接調(diào)用

// 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;
};
...
// 樣式的改變也會(huì)觸發(fā)統(tǒng)一事件
ui.fireEvent(new mxEventObject('styleChanged', 'keys', [mxConstants.STYLE_ROUNDED, mxConstants.STYLE_CURVED],
  'values', ['0', '0'], 'cells', graph.getSelectionCells()));
      
// 監(jiān)聽處理的地方src/main/webapp/js/grapheditor/EditorUi.js 710L
this.addListener('styleChanged', mxUtils.bind(this, function(sender, evt)

無內(nèi)置,可參考相關(guān)邏輯自定義

定制連線樣式

修改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'];

定位相關(guān)邏輯

// 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()

定制控件交互性

業(yè)務(wù)需求:控件是不可編輯且不可調(diào)整尺寸的——
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是經(jīng)過邏輯處理后的屬性類型,不一定是定義時(shí)傳入的類型。

// 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枚舉值來定義控件懸浮時(shí)的箭頭按鈕展示杨拐。
控件懸浮箭頭部分源碼見src\main\webapp\js\grapheditor\Graph.js文件中HoverIcons的定義祈餐。

針對于浮層中的控件,源碼見src\main\webapp\js\grapheditor\EditorUi.js文件中的EditorUi.prototype.getCellsForShapePicker的定義哄陶。

?????定義控件并指定坐標(biāo)插入

// 未梳理的代碼帆阳,拷貝的源碼其他部分
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參數(shù),以_CONFIG_為前綴屋吨,示例內(nèi)容:

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

配置文件的值需要經(jīng)過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
    ...

事件觸發(fā)

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

事件回調(diào)

源碼中,大部分事件都會(huì)拋出一個(gè)自定義事件至扰,在業(yè)務(wù)邏輯上監(jiān)聽該自定義事件鳍徽,即可寫事件回調(diào)。

下面以自定義刪除事件的回調(diào)為例:

//事件監(jiān)聽
  /**
   * 鎖定元素
   */
  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]
    /**
     * 拖動(dòng)Cell時(shí)敢课,也會(huì)觸發(fā)removeCells事件阶祭,此時(shí)eventTarget === undefined
     * 刪除Cell時(shí),觸發(fā)removeCells事件翎猛,此時(shí)eventTarget為刪除的Cell實(shí)例
     */
    if (!eventTarget) return
    var deleteCellStyleProperties = graph.getCurrentCellStyle(eventTarget)
    if (deleteCellStyleProperties.shape === 'label') return
    /**
     * 該邏輯會(huì)走兩次
     * 第一次:刪除當(dāng)前Cell觸發(fā)
     * 第二次:該邏輯中deleteAllAction()調(diào)用引發(fā)的觸發(fā)胖翰,第二次需要忽略
     */
    currentInstalledIndicator = null
    graph.getModel().beginUpdate();
    graph.setSelectionCells(Object.values(graph.getModel().cells))
    lockUnlockAction()
    graph.clearSelection()
    graph.getModel().endUpdate();
    setTimeout(() => {
      /**
       * 該邏輯要延時(shí)執(zhí)行,因?yàn)閘ockUnlockAction的狀態(tài)無法同步更新的view.state中切厘,倒是deleteAllAction中的是否可刪除判斷邏輯無法執(zhí)行后續(xù)
       */
      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的刪除鍵在只選擇一個(gè)控件時(shí)疫稿,是被禁用的

// 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即可。

側(cè)邊欄的事件

縮略圖拖拽事件

// src/main/webapp/js/grapheditor/Sidebar.js
// 初始化側(cè)邊欄面板后遗座,首次展開Palette時(shí)進(jìn)行綁定舀凛,調(diào)用的函數(shù)順序
Sidebar.prototype.createItem = 
...
Sidebar.prototype.createDropHandler = 
....
Sidebar.prototype.createDragPreview = 
...
Sidebar.prototype.isDropStyleEnabled = 

縮略圖點(diǎn)擊事件

// src/main/webapp/js/diagramly/sidebar/Sidebar.js
// 該事件覆蓋了src/main/webapp/js/grapheditor/Sidebar.js中的對應(yīng)事件
Sidebar.prototype.itemClicked = function(cells, ds, evt) {
...

其他對象事件觸發(fā)

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

調(diào)用彈窗

// Dialog確認(rèn)框
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)
// 內(nèi)置Confirm
EditorUi.prototype.confirm = function(msg, okFn, cancelFn)
{
  if (mxUtils.confirm(msg))
  {
    if (okFn != null)
    {
      okFn();
    }
  }
  else if (cancelFn != null)
  {
    cancelFn();
  }
};
this.editorUi.confirm(
  '確認(rèn)么', 
  function() {
    // ok
  }
)
// Error提示框
  this.editorUi.showError(mxResources.get('error'), mxResources.get('notInOffline'));
  // showAlert(message)
  // showError(title, message)
  //showSplash
  //showWarning

節(jié)點(diǎn)事件調(diào)用

添加節(jié)點(diǎn)

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

刪除節(jié)點(diǎn)

// 獨(dú)立節(jié)點(diǎn),沒有連接線
graph.deleteCells([insertCell], false)

添加/修改文本

// 以連線文本為例途蒋,lineCell.contructor === mxCell猛遍,與事件無關(guān)
graph.labelChanged(lineCell, 'TEST', false)
// 針對事件target、client X| Y插入文本号坡,與事件對象掛鉤
graph.insertTextForEvent(evt, cell)
// 底層mxcell設(shè)置文本
mxCell.getValue()
mxCell.setValue(newV)
mxCell.valueChange: (newV) => oldV

添加/修改事件監(jiān)聽

Graph.prototype.cellLabelChanged = 
....

國際化語言支持

window.mxLanguageMap = window.mxLanguageMap ||修改該項(xiàng)可以調(diào)整右上角的語言選項(xiàng)懊烤,通過URL Query String配置lang可以指定某一語言。

默認(rèn)語言設(shè)置

方案一:全局變量

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

方案二:參數(shù)對象默認(rèn)值

// 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';

隱藏語言切換按鈕

需注釋掉相關(guān)代碼塊

// 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')
// 默認(rèn)文件名 
// src/main/webapp/js/diagramly/EditorUi.js   
this.defaultFilename = mxResources.get('untitledDiagram');

禁止修改文件名

// src/main/webapp/js/diagramly/LocalFile.js
// isRenamable函數(shù)返回false時(shí)宽堆,無法通過交互修改文件名腌紧,能通過js修改
LocalFile.prototype.isRenamable = function()
{
  return false;
};

刪除初始化跳轉(zhuǎn)頁

image.png

注釋掉src/main/webapp/index.html相應(yīng)的結(jié)構(gòu)

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

修改標(biāo)題結(jié)構(gòu)與樣式

// 結(jié)構(gòu)生成: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';

隱藏便簽本

注釋掉相關(guān)構(gòu)造器的調(diào)用即可:

 // 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();
  }

便簽本構(gòu)造器為StorageLibrary

有三個(gè)位置調(diào)用src/main/webapp/js/diagramly/App.js畜隶、src/main/webapp/js/diagramly/sidebar/Sidebar.js壁肋、src/main/webapp/js/diagramly/EditorUi.js号胚。

該側(cè)邊欄的便簽本是在App.js中注冊的,在邏輯中屬于必插入的側(cè)邊欄資源庫浸遗,和loadLibraries掛鉤猫胁,相關(guān)邏輯在src/main/webapp/js/diagramly/App.js5477L~5567L

隱藏存儲(chǔ)選項(xiàng)彈窗

解決方案:

修改彈窗邏輯乙帮,不生成結(jié)構(gòu)杜漠,直接渲染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;
  }

彈窗結(jié)構(gòu):

// src/main/webapp/js/diagramly/Dialogs.js
var StorageDialog = function(editorUi, fn, rowLimit)
{
...
 // 稍后再?zèng)Q定的邏輯
  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;
  });
 ...

彈窗調(diào)用邏輯:

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

實(shí)踐

// 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腳本驾茴?
    因?yàn)榇a依賴問題,注釋掉的話氢卡,需要調(diào)整其它JS腳本的邏輯锈至。

隱藏工具欄Toolbar

實(shí)踐

// 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腳本译秦?
    因?yàn)榇a依賴問題峡捡,注釋掉的話,需要調(diào)整其它JS腳本的邏輯筑悴。

隱藏配置面板Format

實(shí)踐

控件的配置面板源碼在Format.prototype.immediateRefresh的定義中们拙,這里我們不去調(diào)用immediateRefresh函數(shù),即不會(huì)生成各項(xiàng)配置面板——空面板阁吝,但砚婆,空面板寬度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);
  }));
};

為什么

  • 不采用以下方式隱藏——默認(rèn)UI模式下可以的装盯,min模式下會(huì)報(bào)錯(cuò)。
// src\main\webapp\js\grapheditor\EditorUi.js
EditorUi.prototype.createFormat = function(container)
{
  // return new Format(this, container);
};

隱藏共享按鈕

需注釋掉相關(guān)代碼塊

// 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);

按鈕沒有了,就把高度設(shè)置為0吧定躏!

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

隱藏分頁

需注釋掉相關(guān)代碼塊

// src/main/webapp/js/grapheditor/EditorUi.js


if (this.container != null && this.tabContainer != null)
{
 // this.container.appendChild(this.tabContainer);
}

隱藏狀態(tài)提醒

// 結(jié)構(gòu)生成:src/main/webapp/js/grapheditor/EditorUi.js
EditorUi.prototype.createStatusContainer = function()
{
  var container = document.createElement('a');
  container.className = 'geItem geStatus';

  return container;
};
// 狀態(tài)設(shè)置
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);
  }
};
// 狀態(tài)監(jiān)聽
this.editor.addListener('statusChanged', mxUtils.bind(this, function()
{
  this.setStatusText(this.editor.getStatus());
}));

繪圖結(jié)構(gòu)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>

轉(zhuǎn)成對應(yīng)的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版本為例,下載解壓后痊远,切換到解壓后的目錄副编,依次運(yùn)行build.batbuild.sh录煤,bootstrap.batbootstrap.sh且警。

切換到項(xiàng)目目錄下橘荠,ant -file ./etc/build/build.xml幌衣,執(zhí)行結(jié)束后會(huì)替換原有的線上文件矾削。

詳情見github官方文檔壤玫。

Ant命令行使用說明

Notes:

  • 切換到解壓后的目錄
    • 必須執(zhí)行哼凯,該腳本內(nèi)的訪問路徑是相對路徑欲间,必須切換到解壓后的目錄執(zhí)行腳本
  • ant -file ./etc/build/build.xml
    • Mac OS需要將ant設(shè)置為全局,win執(zhí)行腳本時(shí)默認(rèn)了全局
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末断部,一起剝皮案震驚了整個(gè)濱河市猎贴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蝴光,老刑警劉巖她渴,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔑祟,居然都是意外死亡趁耗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門疆虚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苛败,“玉大人,你說我怎么就攤上這事径簿“涨” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵篇亭,是天一觀的道長缠捌。 經(jīng)常有香客問我,道長暗赶,這世上最難降的妖魔是什么鄙币? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蹂随,結(jié)果婚禮上十嘿,老公的妹妹穿的比我還像新娘。我一直安慰自己岳锁,他們只是感情好绩衷,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著激率,像睡著了一般咳燕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乒躺,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天招盲,我揣著相機(jī)與錄音,去河邊找鬼嘉冒。 笑死曹货,一個(gè)胖子當(dāng)著我的面吹牛咆繁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顶籽,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼玩般,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了礼饱?” 一聲冷哼從身側(cè)響起坏为,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎镊绪,沒想到半個(gè)月后匀伏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镰吆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年帘撰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片万皿。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡摧找,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牢硅,到底是詐尸還是另有隱情蹬耘,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布减余,位于F島的核電站综苔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏位岔。R本人自食惡果不足惜如筛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抒抬。 院中可真熱鬧杨刨,春花似錦、人聲如沸擦剑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惠勒。三九已至赚抡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纠屋,已是汗流浹背涂臣。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留售担,地道東北人赁遗。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓闯估,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吼和。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容