DrawIO二次开发
二次开发DrawIO,将DrawIO作为一个嵌入的画图工具,为本来网站提供绘图功能。
声明
本人前端能力有限,这个文章仅能帮助大家快速了解到各类功能具体对应的代码内容,以便自行进行修改。
因为不是专业做前端的,我的改动方式都是很低级的,不建议参考!!
目录
DrawIO二次开发记录
感谢
感谢zhaodeezhu的分享,使我可以快速了解到这些细节。
二次开发目的
将DrawIO作为一个嵌入的画图工具,为本来网站提供绘图功能。
<iframe src="http://localhost:8081?dev=1" frameborder="0"></iframe>
准备
- 点击前往DrawIO下载源码
-
进入目录
drawio-dev\src\main\webapp
-
启用项目
python -m http.server 8081
-
访问项目
localhost:8081
-
若希望及时见到修改结果,请访问
localhost:8081?dev=1
-
若希望去除
dev=1
,则需要在每次修改完毕后,进入目录drawio-dev/etc/build
执行命令ant
(需本地安装jdk、ant)【此方法仅限于使用java启动项目时,如果你和我一样使用python,就老老实实加上dev=1吧】-
我这里会报错:
-
打开
App.js
,搜索loadScripts
,找到以下位置做修改 -
// App.loadScripts(['webapp/js/shapes-14-6-5.min.js', 'webapp/js/stencils.min.js', 'webapp/js/extensions.min.js'], realMain); App.loadScripts(['js/shapes-14-6-5.min.js', 'js/stencils.min.js', 'js/extensions.min.js'], realMain);
-
-
修改过程
简单配置
首先修改配置文件src\main\webapp\js\PreConfig.js
- 设置默认语言
urlParams['lang'] ='zh';
- 设置绘图文件仅能存储在本地
urlParams['od'] = 0;
urlParams['gh'] = 0;
urlParams['gl'] = 0;
urlParams['db'] = 0;
urlParams['gapi'] = 0;
- 修改加载页面的欢迎文字
src\main\webapp\index.html
,搜索geBlock
,修改其中内容
修改可创建的文件类型
drawio-dev/src/main/webapp/js/digramly/Dialogs.js
- 找到如下位置可修改默认选择创建的文件类型
// var ext = '.drawio';
var ext = '.html';
- 修改文件类型的选择框,我这里改为了只允许创建HTML类型【因为暂时只弄懂html格式下,获取图片编码的方法】
- 为了防止用户导入文件时创建drawio格式,也可以修改
EditorUi.js
文件下的openFileHandle
方法,将里面的.drawio
都改成.html
/**
* Known file types.
*/
// Editor.prototype.diagramFileTypes = [
// {description: 'diagramXmlDesc', extension: 'drawio', mimeType: 'text/xml'},
// {description: 'diagramPngDesc', extension: 'png', mimeType: 'image/png'},
// {description: 'diagramSvgDesc', extension: 'svg', mimeType: 'image/svg'},
// {description: 'diagramHtmlDesc', extension: 'html', mimeType: 'text/html'},
// {description: 'diagramXmlDesc', extension: 'xml', mimeType: 'text/xml'}];
Editor.prototype.diagramFileTypes = [
{description: 'diagramHtmlDesc', extension: 'html', mimeType: 'text/html'}];
创建的文件时的设定
drawio-dev/src/main/webapp/js/digramly/Editor.js
无关紧要的修改。
- 不显示以下默认的创建文件对话框
- 直接显示以下对话框
// 大约2653行
//if (!noDialogs)
//{
// this.showSplash();
//}
// 修改为先显示创建何种文件的对话框
if (!noDialogs)
{
_this = this //辅助解决this指向问题
var compact = this.isOffline()
var dlg = new NewDialog(this, compact, !(this.model == App.MODE_DEVICE && 'chooseFileSystemEntries' in window));
this.showDialog(dlg.container, (compact) ? 350 : 620, (compact) ? 70 : 460, true, true, function(cancel){
// 取消时可以重新显示
if (cancel && _this.getCurrentFile() == null){
_this.showSplash();
}
});
dlg.init()
}
创建时不必先保存文件至本地
drawio-dev/src/main/webapp/js/digramly/Dialogs.js
,搜索关键字editorUi.createFile(title, templateXml
editorUi.pickFolder(editorUi.mode, function(folderId)
{
// editorUi.createFile(title, templateXml, (templateLibs != null &&
// templateLibs.length > 0) ? templateLibs : null, null, function()
// {
// editorUi.hideDialog();
// }, null, folderId, null, (templateClibs != null &&
// templateClibs.length > 0) ? templateClibs : null);
//创建临时文件,我们自己定义在App.js
editorUi.createTemporaryFile(title, templateXml, (templateLibs != null &&
templateLibs.length > 0) ? templateLibs : null, null, function(){
editorUi.hideDialog(); // 隐藏选择创建文件的面板
}, null, folderId, null, (templateClibs != null && templateClibs.length > 0) ? templateClibs : null);
}
drawio-dev/src/main/webapp/js/digramly/App.js
,自己选择合适的位置定义如下方法
App.prototype.createTemporaryFile = function(title, data, libs, mode, done, replace, folderId, tempFile, clibs) {
data = (data != null) ? data : this.emptyDiagramXml;
this.fileCreated(new LocalFile(this, data, title, false), libs, replace, done, clibs);
}
导出URL时,导出地址为本地地址
默认代码导出的为DrawIO官网的远程地址
drawio-dev/src/main/webapp/js/digramly/EditorUI.js
,搜索关键字createLink
修改该方法返回值
// return ((lightbox && urlParams['dev'] != '1') ? EditorUi.lightboxHost :
// (((mxClient.IS_CHROMEAPP || EditorUi.isElectronApp ||
// !(/.*\.draw\.io$/.test(window.location.hostname))) ?
// EditorUi.drawHost : 'https://' + window.location.host))) + '/' +
// ((params.length > 0) ? '?' + params.join('&') : '') + data;
return 'http://' + window.location.host + '/' + ((params.length > 0) ? '?' + params.join('&') : '') + data;
实现页面通信
注意:我所实现的是基于我的主页面是利用iframe嵌入DrawIO
做好接收信息的接口
- 这里利用
postMessage
进行通信
drawio-dev/src/main/webapp/js/digramly/App.js
,在App
构造方法内(约180行)添加监听postMessage
的事件。
const listener = mxUtils.bind(this, function(e) {
var data = e.data || {};
if(data.type == 'edit') {
this.hideDialog();
this.updateTemporaryFile(data.title, data.data);
} else if (data.type === 'create') {
this.hideDialog();
this.selectTemplateCreate();
}
})
window.addEventListener('message', listener);
- 定义更新当前页面的方法
App.prototype.updateTemporaryFile = function(title, data) {
data = (data != null) ? data : this.emptyDiagramXml;
// 根据传来的data数据创建一个本地文件
this.fileTemporaryUpdated(new LocalFile(this, data, title, false));
}
// 下面这个方法基本是抄的源码中的fileCreated方法
App.prototype.fileTemporaryUpdated = function(file, libs, replace, done, clibs) {
var url = window.location.pathname;
if (libs != null && libs.length > 0)
{
url += '?libs=' + libs;
}
if (clibs != null && clibs.length > 0)
{
url += '?clibs=' + clibs;
}
url = this.getUrl(url);
// Always opens a new tab for local files to avoid losing changes
if (file.getMode() != App.MODE_DEVICE)
{
url += '#' + file.getHash();
}
// Makes sure to produce consistent output with finalized files via createFileData this needs
// to save the file again since it needs the newly created file ID for redirecting in HTML
if (this.spinner.spin(document.body, mxResources.get('inserting')))
{
var data = file.getData();
var dataNode = (data.length > 0) ? this.editor.extractGraphModel(
mxUtils.parseXml(data).documentElement, true) : null;
var redirect = window.location.protocol + '//' + window.location.hostname + url;
var node = dataNode;
var graph = null;
// Handles special case where SVG files need a rendered graph to be saved
if (dataNode != null && /\.svg$/i.test(file.getTitle()))
{
graph = this.createTemporaryGraph(this.editor.graph.getStylesheet());
document.body.appendChild(graph.container);
node = this.decodeNodeIntoGraph(node, graph);
}
file.setData(this.createFileData(dataNode, graph, file, redirect));
if (graph != null)
{
graph.container.parentNode.removeChild(graph.container);
}
var complete = mxUtils.bind(this, function()
{
this.spinner.stop();
});
var fn = mxUtils.bind(this, function()
{
complete();
var currentFile = this.getCurrentFile();
if (replace == null && currentFile != null)
{
replace = !currentFile.isModified() && currentFile.getMode() == null;
}
var fn3 = mxUtils.bind(this, function()
{
window.openFile = null;
this.fileLoaded(file);
if (replace)
{
file.addAllSavedStatus();
}
if (libs != null)
{
this.sidebar.showEntries(libs);
}
if (clibs != null)
{
var temp = [];
var tokens = clibs.split(';');
for (var i = 0; i < tokens.length; i++)
{
temp.push(decodeURIComponent(tokens[i]));
}
this.loadLibraries(temp);
}
});
var fn2 = mxUtils.bind(this, function()
{
if (replace || currentFile == null || !currentFile.isModified())
{
fn3();
}
});
if (done != null)
{
done();
}
// Opens the file in a new window
if (replace != null && !replace)
{
// Opens local file in a new window
if (file.constructor == LocalFile)
{
window.openFile = new OpenFile(function()
{
window.openFile = null;
});
window.openFile.setData(file.getData(), file.getTitle(), file.getMode() == null);
}
if (done != null)
{
done();
}
fn3();
}
else
{
fn2();
}
});
// Updates data in memory for local files
if (file.constructor == LocalFile)
{
fn();
}
else
{
file.saveFile(file.getTitle(), false, mxUtils.bind(this, function()
{
fn();
}), mxUtils.bind(this, function(resp)
{
complete();
if (resp == null || resp.name != 'AbortError')
{
this.handleError(resp);
}
}));
}
}
}
保存时发送信息
drawio-dev/src/main/webapp/js/digramly/App.js
App.prototype.saveFile = function(forceDialog, success)
{
var file = this.getCurrentFile();
file.updateFileData();
const xml = file.data;
// 若创建文件不是Html格式则此处会报错,若前面与我一样限定只能创建Html格式,则可忽视该注释
// 因为我们要实现的是将绘好的图作为Dom元素插入到父页面
// 因此这里使用Html格式更好处理,其它文件格式的处理方式请自行研究~
const data = xml.match(/data\-mxgraph=\"([\d\w\W]+)\}\"\>\</)[1] + '}';
// top 即 window.top,用于定义父页面,这里利用postMessage给父页面传递信息
top && top.postMessage({
type: 'drawio',
data: data
}, '*');
return;
父页面发送信息
- Html结构
<!-- 包一层盒子,这样可以在此为页面中每个图片设定id,方便定位 -->
<div class="picItem" id="001">
<div onclick="toEdit('001')" class="mxgraph" style="max-width:100%;border:1px solid transparent;" data-mxgraph="#"></div>
</div>
<!-- 父页面从本地引入该脚本 -->
<script type="text/javascript" src="./viewer-static.min.js"></script>
window.$currentXml = null; // 为方便后续操作
iwin = document.getElementById("iframe_id").contentWindow; // iframe_id替换为你的Id
// 编辑已有绘图
function toEdit(id){
detail = document.getElementById(id).getElementsByClassName('geDiagramContainer')[0].getAttribute('data-mxgraph');
xml = JSON.parse(detail).xml
msg = {
type: 'edit',
data: xml,
title: '编辑.html'
}
$currentXml = id;
iwin.postMessage(msg, '*')
}
// 创建新绘图
function CreateFile(){
msg = {
type: 'create',
data: null,
title: '新建.html'
}
$currentXml = null;
iwin.postMessage(msg, '*')
}
父页面接收消息
window.addEventListener("message", function(e){
// 若没有记录Id,则是要创建新绘图
if(!$currentXml){
newId = createId() //请自行定义一个创建Id的方法
newDom = '<div class="picItem" id='+newId+'><div class="mxgraph" οnclick="toEdit(\''+newId+'\')" style="max-width:100%;border:1px solid transparent;" data-mxgraph="'+e.data.data+'"></div></div>'
picBox.insertAdjacentHTML("beforeEnd", newDom)
}
// 有记录Id,是编辑该Id指向的图
else{
oldDom = document.getElementById($currentXml);
newDom = document.createElement("div")
newDom.id = $currentXml
newDom.innerHTML = '<div class="mxgraph" οnclick="toEdit(\''+$currentXml+'\')" style="max-width:100%;border:1px solid transparent;" data-mxgraph="'+e.data.data+'"></div>'
oldDom.parentNode.replaceChild(newDom, oldDom) // 替换原有Dom
}
GraphViewer.processElements() // 加入原生Dom后需要调用官方提供的方法进行处理
});
父页面操作加载好的图片
主要修改viewer-static.min.js
-
建议不添加其默认的选项
- 搜索
this.graph.setPanning(!1);null!=this.graphConfig.toolbar?
,将其后方执行addToolbar的代码修改为true。
- 搜索
-
存储Graph、Viewr实例
- 在脚本最上方定义
var myGraph={}; var myViewr={};
- 搜索
this.graph=new Graph(b);
,在其后方添加:
- 在脚本最上方定义
myViewr[this.graph.container.parentElement.id]=this;
myGraph[this.graph.container.parentElement.id]=this.graph;
- 自行创建按钮实现各种展示效果
<button onclick="CreateFile()">新建图片</button>
<button onclick="ZoomIn('001')">测试放大图</button>
<button onclick="ZoomOut('001')">测试缩小图</button>
<button onclick="RecoverGraph('001')">测试复原图</button>
<button onclick="ShowBigPic('001')">测试看大图</button>
function RecoverGraph(id){
myGraph[id].view.scaleAndTranslate(cjx_graph[id].initialViewState.scale,
myGraph[id].initialViewState.translate.x,
myGraph[id].initialViewState.translate.y);
}
function ShowBigPic(id){
myViewr[id].showLightbox()
}
function ZoomOut(id){
myGraph[id].zoomOut()
}
function ZoomIn(id){
myGraph[id].zoomIn()
}
深入mxGraph——自己用代码控制创建图片
问题:DrawIO是如何创建新图形加入到画布中的?
探索过程
sidebar.js
,第2847行,dropHandler.apply(this, argument)
是在执行创建方法。该方法定义在第2303行createDropHandler
内的return
处。- 2364行的
importCells
即执行插入方法,将被选择的cell
插入到画布上的位置(x, y)
处。 - 2164行
createItem
是在创建左侧菜单栏中的形状,让dropHandler
可以克隆这些item并添加到画布中 - 搜索关键字
fns
可以找到统一创建模板的各种方法。
解决方案
scr/main/webapp/js/grapheditor/Sidebar.js
-
// 定义存储数据的集合 // 可能源码已经给出了如何定位这些东西的方法,读者可以自行研究 // 本人没发现这些方法,故使用笨办法来存储 window.defineCell = {}; window.$currentGraph = null; window.$entrySet = {};
-
搜索
Sidebar.prototype.createItem
-
//方法内加入以下内容,存储定义 // if(defineCell[title]){ i = 1 while(defineCell[title + " " + i.toString()]) i++; title = title + " " + i.toString(); } defineCell[title] = cells;
-
搜索
function Sidebar(editorUi, container)
-
window.$currentGraph = editorUi.editor.graph;
-
// 自定义方法,添加顶点,调用实例: // myAddCell(defineCell["Rectangle"], '实体A', 100, 200, "A"); // myAddCell(defineCell["Rectangle"], '实体B', 100, 350, "B"); function myAddCell(cells, tag, x=0, y=0, val="") { graph = window.$currentGraph; cells = graph.getImportableCells(cells); graph.stopEditing(); graph.model.beginUpdate(); try { x = Math.round(x); y = Math.round(y); // val为形状中显示的值 cells[0].setValue(val); select = graph.importCells(cells, x, y, null); // 设tag为目标图形的key值 window.$entrySet[tag]=select; } catch (e) { // 这里自行决定如何处理错误信息 } finally { graph.model.endUpdate(); } if (select != null && select.length > 0) { // 滚动到能看到添加的位置,并选择刚刚添加的形状 graph.scrollCellToVisible(select[0]); graph.setSelectionCells(select); } };
-
// 自定义方法,添加边,调用实例: // _A = window.$entrySet['实体A']; // _B = window.$entrySet['实体B']; // myAddEdge(_A, _B); function myAddEdge(source, target, val="") { var graph = window.$currentGraph; graph.model.beginUpdate(); try { graph.insertEdge(null, null, val, source[0], target[0], graph.createCurrentEdgeStyle()) } catch(e){ // 这里自行决定如何处理错误信息 } finally{ graph.model.endUpdate(); } }
自定义预定义图形
src\main\webapp\js\grapheditor\Sidebar.js
里面部分方法实际不会被运行,优先级更高的是sidebar文件夹内定义的方法src\main\webapp\js\diagramly\sidebar\Sidebar.js
实际的初始化是在这里被定义src\main\webapp\js\diagramly\sidebar
这里面定义的方法,优先级更高
其它
新增自定义方法
src/main/webapp/js/diagramly/App.j
/** 选择模板创建 */
App.prototype.selectTemplateCreate = function() {
this.setCurrentFile(null);
var compact = this.isOffline();
// editorUi.mode = 'device'
var dlg = new NewDialog(this, compact, !(this.mode == App.MODE_DEVICE && 'chooseFileSystemEntries' in window));
this.showDialog(dlg.container, (compact) ? 350 : 620, (compact) ? 70 : 460, true, true, function(cancel)
{
// this.sidebar.hideTooltip();
if (cancel && this.getCurrentFile() == null)
{
this.showSplash();
}
});
dlg.init();
}
/** 创建临时文件 */
App.prototype.createDaokeFile = function(title, data, libs, mode, done, replace, folderId, tempFile, clibs) {
// console.log(data)
data = (data != null) ? data : this.emptyDiagramXml;
// console.log(replace);
// console.log(new LocalFile(this, data, title, false));
this.fileCreated(new LocalFile(this, data, title, false), libs, replace, done, clibs);
}
/** 更新当前图文件 */
App.prototype.updateDaokeFile = function(title, data) {
// console.log('@@@', data)
data = (data != null) ? data : this.emptyDiagramXml;
// console.log(title, data);
// console.log(new LocalFile(this, data, title, false));
this.fileDaokeUpdated(new LocalFile(this, data, title, false));
}
App.prototype.fileDaokeUpdated = function(file, libs, replace, done, clibs) {
var url = window.location.pathname;
if (libs != null && libs.length > 0)
{
url += '?libs=' + libs;
}
if (clibs != null && clibs.length > 0)
{
url += '?clibs=' + clibs;
}
url = this.getUrl(url);
// Always opens a new tab for local files to avoid losing changes
if (file.getMode() != App.MODE_DEVICE)
{
url += '#' + file.getHash();
}
// Makes sure to produce consistent output with finalized files via createFileData this needs
// to save the file again since it needs the newly created file ID for redirecting in HTML
if (this.spinner.spin(document.body, mxResources.get('inserting')))
{
var data = file.getData();
var dataNode = (data.length > 0) ? this.editor.extractGraphModel(
mxUtils.parseXml(data).documentElement, true) : null;
var redirect = window.location.protocol + '//' + window.location.hostname + url;
var node = dataNode;
var graph = null;
// Handles special case where SVG files need a rendered graph to be saved
if (dataNode != null && /\.svg$/i.test(file.getTitle()))
{
graph = this.createTemporaryGraph(this.editor.graph.getStylesheet());
document.body.appendChild(graph.container);
node = this.decodeNodeIntoGraph(node, graph);
}
file.setData(this.createFileData(dataNode, graph, file, redirect));
if (graph != null)
{
graph.container.parentNode.removeChild(graph.container);
}
var complete = mxUtils.bind(this, function()
{
this.spinner.stop();
});
var fn = mxUtils.bind(this, function()
{
complete();
var currentFile = this.getCurrentFile();
if (replace == null && currentFile != null)
{
// console.log('我被执行了----6')
replace = !currentFile.isModified() && currentFile.getMode() == null;
}
// console.log('我被执行了----1')
var fn3 = mxUtils.bind(this, function()
{
// console.log('我被执行了----7')
window.openFile = null;
this.fileLoaded(file);
if (replace)
{
file.addAllSavedStatus();
}
if (libs != null)
{
this.sidebar.showEntries(libs);
}
if (clibs != null)
{
var temp = [];
var tokens = clibs.split(';');
for (var i = 0; i < tokens.length; i++)
{
temp.push(decodeURIComponent(tokens[i]));
}
this.loadLibraries(temp);
}
});
// console.log('我被执行了----2')
var fn2 = mxUtils.bind(this, function()
{
// console.log('我被执行了----3')
if (replace || currentFile == null || !currentFile.isModified())
{
// console.log('我被执行了----5')
fn3();
}
else
{
// console.log('我被执行了----4')
// zhaodeezhu 提示跳转
// this.confirm(mxResources.get('allChangesLost'), null, fn3,
// mxResources.get('cancel'), mxResources.get('discardChanges'));
}
});
if (done != null)
{
done();
}
// Opens the file in a new window
if (replace != null && !replace)
{
// Opens local file in a new window
if (file.constructor == LocalFile)
{
window.openFile = new OpenFile(function()
{
window.openFile = null;
});
window.openFile.setData(file.getData(), file.getTitle(), file.getMode() == null);
}
if (done != null)
{
done();
}
// console.log('我被执行了----9');
fn3();
// zhaodeezhu
// window.openWindow(url, null, fn2);
}
else
{
fn2();
}
});
// Updates data in memory for local files
if (file.constructor == LocalFile)
{
fn();
}
else
{
file.saveFile(file.getTitle(), false, mxUtils.bind(this, function()
{
fn();
}), mxUtils.bind(this, function(resp)
{
complete();
if (resp == null || resp.name != 'AbortError')
{
this.handleError(resp);
}
}));
}
}
}
待继续……【有可能不会继续了,以上内容已经基本满足我的需求】
更多推荐










所有评论(0)