DrawIO二次开发

本文详细记录了对DrawIO的二次开发过程,包括设置默认语言、限制文件类型、禁用保存至云端、实现页面间通信、创建临时文件、导出本地URL等功能。此外,还探讨了如何通过代码控制图形创建,以及在前端框架中嵌入DrawIO的交互方式。整个开发旨在将DrawIO作为内置绘图工具,提供给网站用户使用。
摘要由CSDN通过智能技术生成

声明

本人前端能力有限,这个文章仅能帮助大家快速了解到各类功能具体对应的代码内容,以便自行进行修改。
因为不是专业做前端的,我的改动方式都是很低级的,不建议参考!!

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

待继续……【有可能不会继续了,以上内容已经基本满足我的需求】

  • 13
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值