X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题

最近在这个 X6 ER 图示例里,我修了一个看起来不大、但实际很容易踩坑的交互问题:

  1. 鼠标悬浮到一条边上时,希望这条边显示到更上层,避免被其他元素压住。

这篇文章记录一下这个问题的成因,以及我最后采用的修复方案。

现象

在 ER 图里,边默认是普通灰色;鼠标移动到边上时,会切换为高亮色。

但如果只做最直接的悬浮效果,会遇到两个问题:

  1. 当前悬浮的边可能仍然被别的节点或边压着,视觉上不够明显。
  2. 如果在 mouseenter 里把边提到更高层,DOM / View 层级发生变化后,原本那条边的 mouseleave 事件有概率丢失。

丢失之后的表现是:

  1. 边已经没有被鼠标悬浮,但仍然保持高亮。
  2. 再移动到其他边时,会出现多个边状态不一致的情况。

修复方案

1. 记录每条边的初始层级

为了让边在悬浮结束后能回到原始位置,先缓存每条边最初的 z-index

typescript 复制代码
const edgeInitialZIndexMap = new Map<string, number>()

const cacheEdgeInitialZIndex = (edge: Edge) => {
  if (!edge || edgeInitialZIndexMap.has(edge.id)) return
  edgeInitialZIndexMap.set(edge.id, edge.getZIndex())
}

这样后面无论边被提到多高,都可以恢复回来,而不是简单写死成某个固定层级。

2. 用一个全局状态记录"当前真正处于悬浮中的边"

这里增加了一个单一状态源:

csharp 复制代码
let pointerHoverEdgeId: string | null = null

这样悬浮态不再分散在每条边自己的事件回调里,而是统一由这一个状态驱动。

3. 悬浮时提升边层级,并同步样式

我把"高亮颜色 + 提升层级"收敛到一个统一的状态同步函数里:

scss 复制代码
const syncEdgeState = (edge: Edge) => {
  cacheEdgeInitialZIndex(edge)
  const baseZ = getEdgeLayerBaseZIndex()

  if (pointerHoverEdgeId === edge.id) {
    applyEdgeHoverStyle(edge)
    edge.setZIndex(baseZ + 1)
    return
  }

  applyEdgeDefaultStyle(edge)
  edge.setZIndex(getEdgeInitialZIndex(edge))
}

这里的关键点是:

  1. 当前悬浮边的层级永远设置成"现有最大层级 + 1"。
  2. 非悬浮边恢复默认样式,并回到自己的初始层级。

4. 保留边级事件处理正常路径

正常情况下,仍然优先使用 X6 自己的边事件:

javascript 复制代码
graph.on('edge:mouseenter', ({ edge }) => {
  if (edge.shape === 'er-relationship') {
    syncHoverFallback(edge.id)
    syncEdgeState(edge)
  }
})

graph.on('edge:mouseleave', ({ edge }) => {
  if (pointerHoverEdgeId === edge.id) {
    pointerHoverEdgeId = null
  }
  syncEdgeState(edge)
})

这部分负责处理绝大多数正常交互路径。

5. 用容器级 mousemove 做兜底

真正解决问题的关键,在于增加了容器级监听。

当边的层级变化后,如果某次 edge:mouseleave 没有触发,就由容器上的 mousemove 去判断"鼠标现在到底悬浮在哪个元素上",然后主动修正状态:

scss 复制代码
const handleContainerMouseMove = (event: MouseEvent) => {
  const nextEdgeId = resolveHoverEdgeId(event.target)
  syncHoverFallback(nextEdgeId)
}

其中 resolveHoverEdgeId 会通过 graph.findViewByElem(target) 反查当前命中的元素是否属于某条边。

syncHoverFallback 的职责是:

  1. 记录新的悬浮边
  2. 找到上一条悬浮边
  3. 如果上一条边不该再悬浮了,就主动把它恢复默认状态

这就把"等待 mouseleave 通知我结束"改成了"我每次都根据当前真实命中结果来纠正状态",稳定性会高很多。

6. 鼠标离开整个容器时,统一清空悬浮状态

除了 mousemove,还补了两个离开容器的兜底:

csharp 复制代码
container.addEventListener('mouseleave', handleContainerMouseLeave)
graph.on('graph:mouseleave', handleContainerMouseLeave)

这样当鼠标直接离开画布区域时,也能确保悬浮态被清掉,不会把高亮残留在最后一条边上。

7. 在 dispose 时清理容器事件

因为这次新增了原生 DOM 事件监听,所以销毁时也要把它们收掉:

ini 复制代码
const originalDispose = graph.dispose.bind(graph)
let hoverFallbackDisposed = false

graph.dispose = (...args: any[]) => {
  if (!hoverFallbackDisposed) {
    container.removeEventListener('mousemove', handleContainerMouseMove)
    container.removeEventListener('mouseleave', handleContainerMouseLeave)
    graph.off('graph:mouseleave', handleContainerMouseLeave)
    hoverFallbackDisposed = true
  }
  originalDispose(...args)
}

这是一个小细节,但很重要。否则组件反复挂载/卸载后,事件可能重复绑定。

代码位置

github.com/adai212/X6D...

相关推荐
李明卫杭州1 小时前
CSS aspect-ratio 属性完全指南
前端
Pedantic3 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘3 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆4 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师5 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆5 小时前
VSCode自动格式化三要素
前端
爱勇宝5 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen6 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程