前言
mxGraph定义了自己完整的事件冒泡和监听机制,但是其既不同于mouseOver,click等原生的DOM事件,也不同于vue中的emit,和v-on等. 本文提供一些mxGraph事件和vue事件整合的思路. 环境:
mxGraph: 4.2.2
vue: 2.6
mxGraph的事件机制
之前的整合el-tree那篇文章中提供了简单的样例,在js中fireEvent
,在vue中addListener
,回顾:
mxGraph中的fireEvent和addListener
自定义事件
相关的对象
mxEventSource
和它的名字一样,表示一个事件的来源,具体讲就是可以fireEvent
的类,原文的注释
Known Subclasses:
<mxGraphModel>, <mxGraph>, <mxGraphView>, <mxEditor>, <mxCellOverlay>, <mxToolbar>, <mxWindow>
也就意味着只有上述的可以通过fireEvent
方法进行事件的主动触发,其他的类只能使用mxGraph库中定义的方法.
mxEvent
这个类里的函数都是对DOM事件方法addEventListener
/removeEventListener
等原生函数的再次封装,你如果想使用mouseup,mousedown等原生DOM事件,可以使用mxEvent
. 比如常用的禁用右键功能mxEvent.disableContextMenu
的源码:
disableContextMenu: function(element)
{
mxEvent.addListener(element, 'contextmenu', function(evt)
{
if (evt.preventDefault)
{
evt.preventDefault();
}
return false;
});
},
第三行调用了addListener处理contextmenu事件,而mxEvent.addListener
又调用了element.addEventListener
:
if (window.addEventListener)
{
return function(element, eventName, funct)
{
element.addEventListener(eventName, funct, false);
updateListenerList(element, eventName, funct);
};
}
所以你可以直接用这个类处理一些原生的DOM事件,注意参数element是DOM元素而不是mxGraph中的对象. 其余的属性都是一些常量,给mxGraph自己使用,比如’add’,'remove’等,见下方固有事件
mxEventObject
我们想连接vue和mxGraph的事件其实很简单,主要的需求是参数的传递,mxEventObject
的作用就在这里,我们先看mxEventSource.fireEvent
的源码注释
函数定义 | 参数1 | 参数2 |
---|---|---|
mxEventSource.prototype.fireEvent = function(evt, sender) | evt - mxEventObject that represents the event | sender- Optional sender to be passed to the listener. Default value is the return value of getEventSource |
代表这个事件的mxEventObject对象 | 事件触发者(可选),默认就是getEventSource的返回值,也就是fireEvent函数的调用者 |
再来看mxEventObject的定义
function mxEventObject(name)
{
this.name = name;
this.properties = [];
for (var i = 1; i < arguments.length; i += 2)
{
if (arguments[i + 1] != null)
{
this.properties[arguments[i]] = arguments[i + 1];
}
}
};
也就是说你可以通过new mxEventObject(“eventName”, key1, val1, .., keyN, valN)
的方式构造你自己的一个事件,源码中参数表只有name,但是for循环中使用arguments
,从i=1也就是第二个开始读取,每次循环i自增2,依次将key,val放进叫做properties
的属性中.
参数封装
比如我想让我定义的画布*mygraph*
触发一个hello事件,传递参数格式为
{
name: "xiaoming",
age: 30
}
那么我就写作:
let evtObj = new mxEventObject('hello','name','xiaoming','age',30);
mygraph.fireEvent(evtObj)
参数获取
通过上面的封装,我的name和age已经放入properties
里,获取很简单,先看源代码:
/**
* Function: getProperties
*
* Returns <properties>.
*/
mxEventObject.prototype.getProperties = function()
{
return this.properties;
};
/**
* Function: getProperty
*
* Returns the property for the given key.
*/
mxEventObject.prototype.getProperty = function(key)
{
return this.properties[key];
};
它提供了两个函数,获取所有参数/获取某个叫做key的参数.还是我自己定义的*mygraph*
对象为例,接收事件和参数分为两步,先定义对hello
事件的监听,再获取里面的参数,这里先看如何增加监听:
函数定义 | 参数1 | 参数2 |
---|---|---|
mxEventSource.prototype.addListener = function(name, funct) | name-mxEventObject的事件名 | funct-对应的回调函数 |
参数2这个回调函数funct的源码注释
The parameters of the listener are the sender and an mxEventObject
也就是说这个回调函数的参数是fireEvent的参数反过来,这里要注意:
xxx.fireEvent(evtObj,sender)
xxx.addListener('hello',function(sender,evtObj){
})
好了,现在可以添加监听器并取出我放在mxEventObject中的两对属性了,这里直接用箭头函数定义回调,和function一样:
mygraph.addListener('hello',(sender,evt)=>{
let mingzi = evt.getProperty('name');
let nianling = evt.getProperty('age');
// 其他后续操作......
})
那第一个参数,事件触发者有什么作用呢?这个就是用来去除同名的事件被多次监听,然后错误地响应,你可以在回调里判断hello是谁发的,我只要我自己的:
mygraph.addListener('hello',(sender,evt)=>{
if(sender === mygraph){
let mingzi = evt.getProperty('name');
let nianling = evt.getProperty('age');
// 其他后续操作......
}
})
yourgraph.addListener('hello',(sender,evt)=>{
if(sender === yourgraph){
let mingzi = evt.getProperty('name');
let nianling = evt.getProperty('age');
// 其他后续操作......
}
})
那如果你这个事件确实是一对多广播的,这个判断就可以丢掉,或者直接避免同名事件的触发.除此之外你可以对sender
进行其他操作,比如打印信息/记录日志等:
yourgraph.addListener('hello',(sender,evt)=>{
console.log(sender.name + "发了个hello事件")
// 如果这个addListener是写在vue的生命周期或者methods中,那你还可以做其他的
this.其他方法(sender.属性) //具体看你的sender是什么
let mingzi = evt.getProperty('name');
let nianling = evt.getProperty('age');
this.后端请求(mingzi,nianling)
// 其他后续操作......
})
整合vue
仅供参考,了解原理之后mxGraph只是个对象,存在哪里/如何读取等可以随意结合
(一)画布只有一个单文件组件使用:
export default{
data:{
mygraph:null
},
mounted(){
//上期讲过的初始化方法
this.mygraph = drawMyOwnGraph(dom元素)
this.mygraph.addListener('hello',(sender,evtObj)=>{
evtObj获取你的参数
this.$emit('给父组件冒泡事件',参数)
this.做其他的,调用methods或者api等
})
this.mygraph.addListener('其他事件',function(sder,evObj){
和上面一样
})
},
}
(二)画布对象好多vue组件都用:
A.vue
<template>A组件的结构</template>
import 部分;
export default{
mounted(){
let yourgraph = drawMyOwnGraph(dom元素)
然后找个全局可读取的地方给它存起来,Session,cookie,storage,IDB等,我这里用vuex
语法看vuex文档
this.$store.dispatch('graph/initGraph',yourgraph)
}
}
//====================================================
//====================================================
B.vue
<template>B组件的结构</template>
import 部分;
export default{
computed:{
这里可以用mapState或者mapGetters等,把vuex中的yourgraph取出来,前提是你定义了
...mapGetters([
'yourgraph'
])
},
mounted(){
然后在你的B组件中就可以用那个画布了,但是你要确保B在A之后挂载
this.yourgraph.mxGraph对象中的函数()........
this.yourgraph.addListener(同上)
}
}
原理
事件监听原理很简单,addListener
就是定义事件名和回调的对应关系,然后放进叫做eventListeners
的属性中:
mxEventSource.prototype.addListener = function(name, funct)
{
if (this.eventListeners == null)
{
this.eventListeners = [];
}
this.eventListeners.push(name);
this.eventListeners.push(funct);
};
而fireEvent
则是找事件名对应的回调并执行:
mxEventSource.prototype.fireEvent = function(evt, sender)
{
if (this.eventListeners != null && this.isEventsEnabled())
{
//省略
var args = [sender, evt];
for (var i = 0; i < this.eventListeners.length; i += 2)
{
var listen = this.eventListeners[i];
if (listen == null || listen == evt.getName())
{
this.eventListeners[i+1].apply(this, args);
}
}
}
};
这个eventListener
是一个数组,也是每隔两个算作一对,和mxEventObject
的参数表key1,val1,key2,val2…类似,它是name1,funct1,name2,funct2…,所以遍历步长是2
固有事件
固有事件有需要的朋友可以私信或者留言,因为大部分都是参数表的翻译,这部分只需要大家熟悉api文档即可,和vue组件的@click @input @blur这些一样,都有固定保留字和一定的写法限制,而且上期我也举了一个重写固有事件回调的例子
画布中元素被删除事件重写
// 这是文档中常用的类似java重载的手段
// 先暂存旧的
let oldcellsRemoved = firstGraph.cellsRemoved
// 再定义新的
firstGraph.cellsRemoved = function(cells){
cells.forEach(iter=>{
console.log(iter.name+'被删掉了')
})
//然后再调用之前的,或者也可以不调用,或者先调用旧的,再新增你自己的逻辑,没有return可以随意调换位置
oldcellsRemoved.call(this,cells)
}
这里我用有元素新增的事件为例,讲一下它的流程,其实源代码中也是使用上面的自定义事件.这里用到了上面的mxEvent
中的那些预定义的常量作为事件名,比如新增的事件名叫做mxEvent.CELLS_ADDED
,
/**
* Variable: CELLS_ADDED
*
* Specifies the event name for cellsAdded.
*/
CELLS_ADDED: 'cellsAdded',
当你在执行yourgraph.insertVertex()
时,先创建一个节点,然后再调用addCell
把它放进画布:
mxGraph.prototype.insertVertex = function(parent, id, value,
x, y, width, height, style, relative)
{
var vertex = this.createVertex(parent, id, value, x, y, width, height, style, relative);
return this.addCell(vertex, parent);
};
在addCell里直接去addCells
,addCells
中开启一次model
更新事务
mxGraph.prototype.addCell = function(cell, parent, index, source, target)
{
return this.addCells([cell], parent, index, source, target)[0];
};
//=============================
mxGraph.prototype.addCells = function(cells, parent, index, source, target, absolute)
{
this.model.beginUpdate();
try
{
this.cellsAdded(cells, parent, index, source, target, (absolute != null) ? absolute : false, true);
this.fireEvent(new mxEventObject(mxEvent.ADD_CELLS, 'cells', cells,
'parent', parent, 'index', index, 'source', source, 'target', target));
}
finally
{
this.model.endUpdate();
}
return cells;
};
能看到,它其实主动调用了mxGraph.cellsAdded
这个有组件新增时的回调,然后使用我们上面讲过的fireEvent
触发mxEvent.ADD_CELLS
这个方法并带了一系列参数,只不过直接在参数里new mxEventObject而已;
然后在cellsAdded
中再真正触发mxEvent.CELLS_ADDED
并携带许多参数:
mxGraph.prototype.cellsAdded = function(cells, parent, index, source, target, absolute, constrain, extend)
{
//省略了源码中对参数的判断和各种操作
this.model.beginUpdate();
try
{
this.fireEvent(new mxEventObject(mxEvent.CELLS_ADDED, 'cells', cells,
'parent', parent, 'index', index, 'source', source, 'target', target,
'absolute', absolute));
}
finally
{
this.model.endUpdate();
}
}
组件新增/删除,画布缩放等这些固有的事件都被提前定义,而且文档中已经告知用户这个事件的触发时机,我们可以直接addListener
进行监听即可,比方说上面的CELLS_ADDED
,
文档中说了
Fires between begin- and endUpdate in cellsAdded.
这和我们看的源码一样,在cellsAdded的update事务中触发
如果你想监听,你只需要像之前的hello一样给这个事件加回调就行了
yourgraph.addListener(mxEvent.CELLS_ADDED,(sender,evtObj)=>{
写自己的逻辑,和hello一模一样,只不过是它固有的而已
})
so,那干嘛不直接改它的回调呢?它都调用cellsAdded
了,你直接在那里改不是更简单吗?
总的来说,一般都是修改固有事件的回调方法进行效果和功能的修改,除非你在新增组件之后不想让它触发mxEvent.CELLS_ADDED
,那你把整个原型prototype方法重写,修改源代码
yourgraph.cellsAdded = function(源码入参复制过来){
源码不修改的部分直接复制过来
this.fireEvent(new mxEventObject('我不想要CELLS_ADDED',参数1,值1,参数2,值2))
然后不调用原有的cellsAdded,就修改好了
其他的事件也是一样,去文档里找它在哪里触发的,把它毙掉就可以,换成你的
}
这里没有讲详细的鼠标/键盘等事件的触发方式,因为那属于对mxGraph对象某些方法的触发,之后会讲到.关于事件的处理有疑问请留言,欢迎讨论.
拖拽的画布如何保存呢? 下期更新: mxGraph整合vue入门之画布xml/mysql/redis持久化