事件代理功能点分享
事件代理功能点分享
看过前面几篇文章的读者相信也应该有所了解了,我们借助于团队内部开发的编辑器实现了很多成功的项目案例,已经多次看到我们点击一个文本、一个按钮等等...去弹出一个表格之类的交互,有些同学难免有些好奇我们是如何实现在 canvas 图形对象上实现事件派发和监听的,接下来听我娓娓道来。
准备工作
其实在实现事件代理对象之前,我实现过一个文本对象,就是在 canvas 上绘制出一个固定默认宽高的矩形,用户双击时可输入文字,它的实现可谓是非常简单。但是其中也有不容忽视的小细节需要注意。
文本对象的实现
双击文本对象时,实现一个临时的 DOM 节点
// 创建input
createTextInput() {
this.setVisible(false)
const textInput = document.createElement("input")
this.c("textInput", textInput)
// 设置基本样式
...
//input刚创建出来并不会自动聚焦
//这里需要调用一次自身focus 让input聚焦
textInput.focus()
const remove = () => {
// 删除Input
this.removeTextInput()
textInput.removeEventListener("blur", remove, false)
}
textInput.addEventListener("blur", remove, false)
}
// 更新input的位置
updateTextInput() {
this.setVisible(false)
const textInput = this.getClient("textInput")
const style = textInput.style
const zoom = this._network.getZoom()
const rect = this._network.getViewRect()
const { x, y } = this.getLocation()
style.display = "block"
style.left = x * zoom + -rect.x + "px"
style.top = y * zoom + -rect.y + "px"
}
记录并存储用户键入的内容和操作
这一步由于我们采用的是 input 所以我们只需要在 input 失去焦点的时候,获取到 input.value 的内容就行,同时也一定要注意要删除掉 input 节点,否则页面上的文本一旦多了起来,会很大程度影响性能.
- 监听用户离开,不再操作临时 DOM 节点,将键入的内容缓存取出设置到
文本对象上 - 在 UI 绘制中控制 canvas 绘制用户输入的内容
- 扩展部分:
暴露出部分可控的接口和方法在外部,用户可在属性面板配置操作轻松实现文字阴影、跑马灯、渐变色...等各种功能
事件代理对象
由此想到了我们是不是也可以实现一个类似的事件代理对象,这样一来我们就能轻松操作我们框架上 canvas 画布里的各种对象了。接下来就让我们一起去瞧瞧团队里是如何实现的事件代理对象。
用什么实现代理
使用虚拟的一个 HTMLNODE 节点代理目标对象事件
// BaseHTMLNode是自己封装的一个原生DOM节点对象
class EventProxyNode extends BaseHTMLNode {
constructor(network) {
// 关联的目标对象
this._attacher = null;
}
...
}
设计的时候要考虑到使用时传入一个目标对象关联绑定
关联目标对象
在这里我们通过绑定事件和派发事件去使得目标对象和代理对象关联
在编辑器中已经预留了派发事件和监听事件的操作面板。
在工具中步骤截图如下:
- 打开事件代理开关
- 在目标对象上派发一个事件 配置派发的事件
- 然后在监听对象绑定监听事件并且写上处理逻辑,例如跳转切换等等...
由于我们在画布交互对象中注册了交互事件监听,每当我们的事件代理对象触发事件时就派发
一个画布的事件出去
- 预览效果
(不知道是哪位小伙伴写的注释,给他点赞 👍)
关于事件代理是如何执行代理的代码如下所示,每次代理对象触发了事件后就通过我们的canvas画布对象把事件派发出去,传递的参数当中是代理对象关联的目标对象,这样就相当于是我们的目标对象触发了这个事件,然后只需要在监听对象上绑定监听写上处理逻辑即可。
/**
* 事件代理网元触发事件的时候把事件派发到宿主网元上
* @param {*} network
*/
function addEventProxyInteraction(network) {
network.addInteractionListener((e) => {
if (e.element instanceof EventProxyNode) {
network.fireInteractionEvent({
// 事件名称
kind: e.kind,
// 事件源
element: e.element._attacher,
// 事件对象
event: e.event,
});
}
});
}
管理事件代理
在这里我们的管理事件代理对象的就是目标对象,当我们初始化渲染页面生成图元时,
就会生成对应的事件代理对象,并且在属性面板预留了一个开关用来控制是否启用事件代理对象
// 创建事件代理对象
createEventProxyNode() {
if (this._eventProxyNode) {
return
}
this._eventProxyNode = new EventProxyNode(this._network)
const parent = this.getParent()
this._eventProxyNode.setParent(parent)
const zoom = this._network.getZoom()
this._eventProxyNode.scaleWithZoom(zoom)
const { x, y, width, height } = this.getRect()
this._eventProxyNode.setLocation(x, y)
this._eventProxyNode.setSize(width, height)
this._eventProxyNode.append()
this._eventProxyNode._attacher = this
}
// 初始化事件代理对象
initProxyNodes() {
const box = this._box
box.forEach(node => {
if (node.getEventProxy && node.getEventProxy()) {
node.createEventProxyNode()
}
})
}
...
结尾总结
这样一来我们就能点击一个明明不是 DOM 节点,也不是我们框架当中的一个内置对象,却依然能够触发一系列的事件,并且还能在配置当中灵活配置相应的事件,方便了我们去实现各种交互。
有时当我们实现一个很小的功能点的时候,可以根据这个功能点放大,宏观的去思考一下有没有可能实现一个通用的、可复用的功能点,功能虽小,实现虽简单,但是却能方便我们做很多事,高楼大厦也是从一砖一瓦来的。
如果对可视化感兴趣,可以和我交流,微信541002349. 另外关注公众号“ITMan彪叔” 可以及时收到更多有价值的文章。